Wednesday, May 20, 2009

Selecting a file from a QUEST library (using SCL)

QUEST SCL provides a means for an SCL programmer to call the standard file selection dialog box, where the user can pick a file from a specific folder from a specific set of libraries. This function is called FILE_POP_UP. You pass in a string or string array containing the directory (or directories) to be shown in the dialog box.

The way I use FILE_POP_UP, I use the inquire_config_path to populate a string array with all the libraries containing a specified folder name, and pass that array to FILE_POP_UP. I've encapsulated this functionality into a single SCL routine you can use:

routine get_filename( folder_name : String ) : String
Var
cpaths : array [50] of String
numpths : Integer
result : String
Begin
inquire_config_path( folder_name + '$LIB',cpaths, numpths)
FILE_POP_UP(cpaths, result)
return result
End

Now rather than havign to remember how to use inquire_config_path along with FILE_POP_UP, I can simply call a routine like so:

sel_file_path = get_filename( 'DATA' )

And of course, when you want the user to be able to select an SCL logic file, you'll want to be able to provide the option of selecting from the LOGICS and SCLMACROS folders from your libraries, so I've created a routine that takes two folder names, and combines their inquire_config_path results into a single array that gets passed to the FILE_POP_UP. Here it is:

routine get_filename_multiple( folder_name1 : String ; folder_name2 : String ) : String
Var
cpaths , cpaths2 : array [50] of String
the_paths : Array[ 100 ] of String
numpths , numpths2 , i , write_idx : Integer
result : String
Begin

inquire_config_path( folder_name1 + '$LIB',cpaths, numpths)
inquire_config_path( folder_name2 + '$LIB',cpaths2, numpths2)
write_idx = 0
for i = 0 to numpths - 1 do
if( cpaths[ i ] <> '' ) then
the_paths[ write_idx ] = cpaths[ write_idx ]
write_idx = write_idx + 1
endif
endfor
for i = 0 to numpths2 - 1 do
if( cpaths2[ i ] <> '' ) then
the_paths[ write_idx ] = cpaths2[ i ]
write_idx = write_idx + 1
endif
endfor
FILE_POP_UP(the_paths, result)
return result

End

You can use it like so:

sel_file_path = get_filename_multiple( 'LOGIC' , 'SCLMACRO' )


Executing shell calls in SCL and BCL

The combination of QUEST BCL and SCL is extremely powerful not only for building and runnign simulation models, but also for providing a better experience for you to use QUEST models. BCL and SCL can allow you to greatly simplify modeling tasks, at the tradeoff of lowering QUEST's flexibility from infinite, to match with some set of assumptions you've come up with.

One powerful tool here, is the ability to generate system shell calls, and here I'll talk about this on Windows machines, only.

QUEST BCL has a call named "SYSTEM" which takes a single string argument, enclosed in single quotes. The QUEST BCL documentation gives an example of copying a file:

SYSTEM 'copy thisfile thatfile'

where thisfile is the path to the file to copy, and thatfile is the path to the file you want to copy to. This is useful enough, and pretty self explanatory, especially if you've used the Windows command line enough.

The example I'd like to provide lays in launching applications, and in this case, Microsoft Excel. To simply launch a file from the command line, all you need to do is feed the command shell the path to the file to open. Windows knows how to handle pretty much any file type you throw at it, and in the traditional GUI use of Windows you're prompted if Windows doesn't know what to do with the file.

This works fine for files where there's only one program for viewing or editing a particular file type. But what happens if you want to chose what program to use? Using the command line shell, we can specify the program file to use to open a file by passing the path of our file as an argument in starting the application. Most major applications support this.

So if you have Open Office set as the default editor of Excel files, passing an Excel file to the command shell will open it in Open Office. But if for some reason you want to open this file in Excel (without changing the default behavior) we just have to start Excel with our file as a command line argument.

So here's an SCL routine you can use to launch an Excel file:

routine launch_excel( file_name : String ) : Integer
Const
EXCEL_LOCATION 'C:/Program Files (x86)/Microsoft Office/Office12/excel.exe'
Var
bclerr : Integer
bclmsg : String
Begin

bclmsg = "SYSTEM '" + EXCEL_LOCATION + " "

bclmsg = bclmsg + file_name

bclmsg = bclmsg + " &'"
if( EXCEL_LOCATION <> '' ) then
bclerr = bcl(bclmsg)
endif
return bclerr
End

All we're doing is using the SYSTEM BCL call with the file_name argument as a commmand line argument. Pretty simple, eh?

Precompiled (always there) SCL routines

Over the course of the last few years, I have developed some SCL utility logics that I use quite often and don't like having to copy over and over again. For example, a routine for replacing strings, or converting text to upper or lower case.

It's nice to have these logics, but normally, if you wanted to use them, you'd have to find a copy of the routines declared somewhere on your computer (possibly using my subroutine indexer). However, QUEST provides a directory within your QUESTlib folder called USERDEF, with a folder for logics that, if present there, will compile every time QUEST starts up.

This means that any procedures and routines within one of those USERDEF files will be available to you without having to copy them to the logic you're working on. They're always there. If you're trying to use a precompiled routine, however, you'll have to remember to declare the routine in the Extern section of your logic file.

What I usually do to cover this is to put in the extern definition for all my routines along with the definition of the routine in the USERDEF library. This way I can just go in and grab the extern def rather than the actual SCL definition.

In fact, to make it even easier to set up the extern definitions for all my routines, I've actually written an SCL macro that will do this, more or less, for me. You can download it at:


Just load this into an SCL macro button, and when executed it will prompt you to select an SCL file. It will then search through the file you selected and export the extern representation for all your routines to a text file. It will then launch notepad and open this file for you.

Then all you have to do is copy the extern definitions. Hopefully that makes life a little easier for you.

Wednesday, May 6, 2009

Excel VBA UDFs for QUEST BCL (wow, enough acronyms?)

If you have any familiarity with Excel VBA, you've probably made at least one User Defined Function (UDF) for use in an Excel spreadsheet. If you're not familiar with it, you basically just create a function within an Excel VBA standard module that may or may not take arguments, and returns something that can be displayed in Excel. For example, here is a simple function that just returns the sum of two arguments.

Function MySum(arg1 As Variant, arg2 As Variant)
MySum = arg1 + arg2
End Function

In this function, arg1 and arg2 are of Variant types, meaning the user is able to pass in a literal number or a selected Excel cell (called a Range). By defining variables as Variants, we can assume that whatever the user passes into the function can be cast into the variable type required by the operators we are using (in this case the addition/concatentation operator +).

What some people don't necessarily think of though, is to use VBA to create string concatenating UDFs. Here's an example that just puts two pieces of text together:

Function MyConcat(arg1 As Variant, arg2 As Variant)
MyConcat = CStr(arg1) & CStr(arg2)
End Function

You can see it's similar to MySum, except that we are using the & operator which is just for string concatenation in this use (where the + operator can be addition or concatenation). Also, we are making use of the built-in VBA function CStr to cast our variant data into strings. This is because the & operator requires strings as operands.

Now to the BCL part. We can use something similar to MyConcat to make it so we can easily create BCL commands by simply passing one or more arguments into specially created UDFs. Here's an example for creating a cycle process:

Public Function create_cycle_process(process_name As Variant) As String
create_cycle_process = "CREATE CYCLE PROCESS '" & process_name & "'"
End Function

So a user only has to pass in the name of the process they want to create, and VBA returns a properly formatted BCL command to execute in QUEST. This can be pretty handy.

Even better though, is that VBA allows optional arguments in UDFs, so we can easily account for the optional arguments in many BCL commands. A good example of this is the CREATE ELEMENT CLASS command, which requires that you provide the element class type and the element class name, but allows you to also specify the number of elements and the geometry file path to use for the class display. Without optional arguments you would have to write four different UDFs to be able to build a CREATE ELEMENT CLASS command with all the possible arguments. But with VBA we can do it one. Here's how:

Public Function create_element_class(class_name, elem_type, Optional num_elem, Optional geo_name)
Dim Result
Result = "CREATE " & elem_type & " CLASS '" & class_name & "'"
If Not IsMissing(num_elem) Then
Result = Result & " NUMBER OF ELEMENTS " & CStr(num_elem)
End If
If Not IsMissing(geo_name) Then
Result = Result & " GEO '" & geo_name & "'"
End If
create_element_class = Result
End Function

This function starts by creating the base BCL command using the required arguments, class_name and elem_type. Then, it uses the VBA function IsMissing to see if our optional arguments were omitted in the function call. If not, they are used to add to the BCL command. Finally, the BCL command is returned to the user. Now the user no longer needs to remember the proper syntax for creating an element class BCL command.

To make it even easier on the user, we can provide higher level functions that provide the proper elem_type for them, so they don't have to remember those either:

Function create_mach_class(class_name , Optional num_elem , Optional geo_name )
create_mach_class = create_element_class(class_name, "MACHINE", num_elem, geo_name)
End Function

Function create_source_class(class_name , Optional num_elem , Optional geo_name )
create_source_class = create_element_class(class_name, "SOURCE", num_elem, geo_name)
End Function

Function create_sink_class(class_name , Optional num_elem , Optional geo_name )
create_sink_class = create_element_class(class_name, "SINK", num_elem, geo_name)
End Function

Function create_buffer_class(class_name , Optional num_elem , Optional geo_name )
create_buffer_class = create_element_class(class_name, "BUFFER", num_elem, geo_name)
End Function

Function create_conveyor_class(class_name , Optional num_elem , Optional geo_name )
create_conveyor_class = create_element_class(class_name, "CONVEYOR", num_elem, geo_name)
End Function

Function create_accessory_class(class_name , Optional num_elem , Optional geo_name )
create_accessory_class = create_element_class(class_name, "ACCESSORY", num_elem, geo_name)
End Function

Function create_agv_controller_class(class_name , Optional num_elem , Optional geo_name )
create_agv_controller_class = create_element_class(class_name, "AGV_CONTROLLER", num_elem, geo_name)
End Function

Function create_labor_controller_class(class_name , Optional num_elem , Optional geo_name )
create_labor_controller_class = create_element_class(class_name, "LABOR_CONTROLLER", num_elem, geo_name)
End Function

Function create_carrier_class(class_name , Optional num_elem , Optional geo_name )
create_carrier_class = create_element_class(class_name, "CARRIER", num_elem, geo_name)
End Function

Function create_agv_class(class_name , Optional num_elem , Optional geo_name )
create_agv_class = create_element_class(class_name, "AGV", num_elem, geo_name)
End Function

Function create_labor_class(class_name , Optional num_elem , Optional geo_name )
create_labor_class = create_element_class(class_name, "LABOR", num_elem, geo_name)
End Function

Function create_path_system_class(class_name , Optional num_elem , Optional geo_name )
create_path_system_class = create_element_class(class_name, "PATH SYSTEM", num_elem, geo_name)
End Function

Hopefully this has given you a good primer on using VBA UDFs to create QUEST BCL commands in Excel, so you can smarter BCL scripts that will be easier to modify and to execute in QUEST.