Using LINDO API Optimization Software from APLX
Contents
About LINDO API
LINDO API is a software library available from Lindo Systems. It is designed to solve a wide range of optimization problems, including linear programs, mixed integer programs, quadratic programs, and general nonlinear non-convex programs.
LINDO API can be called via a .NET wrapper supplied by Lindo, and so can be used from .NET-capable APLs like APLX and Dyalog. The following article discusses calling LINDO API from APLX.
(Readers might like to note that other calling methods are available which don't use .NET. For example, the package also includes a Java interface and so could be used by APLX running on OS X or Linux).
Compiling the wrapper code
The main LINDO API code is shipped as a set of Windows DLLs in binary form, but the .NET wrapper is provided as C# source code. The first step is to compile it to produce a .NET class library.
Compiling the wrapper - Method #1
If you have a copy of Microsoft's Visual Studio, or the free Visual Studio Express, you can use it to compile the LINDO API C# wrapper.
To do this, create a new project and select the "Class Library" template. Add the LINDO API wrapper file to the project. If you installed LINDO API into the default location, this can be found in C:\Lindoapi\include\lindo.cs. Then build the project in the normal way.
In order to use the new class library you need to add it to the APLX .NET search path. This is normally done once, when APLX launches:
s← '.net' ⎕SETUP 'using'
s←s,⊂'Lindo,C:\Users\Simon\Documents\LindoAPL\LindoAPL\bin\Release\LindoLibrary.dll'
'.net' ⎕SETUP (⊂'using'), s Finally, create an instance of the LINDO API class with the following:
lindo←'.net' ⎕new 'lindo'
For more information on compiling C# class libraries for use with APLX, see here.
Compiling the wrapper - Method #2
If you don't have Visual Studio installed you can compile the code dynamically from with APLX. This incurs a slight overhead, but it's barely noticeable.
Here is an APL function to compile the code and return an instance of the 'lindo' class. It makes use of a function MakeNetArray which we'll define in a moment.
R←CompileLindo;CSharpCodeProvider;loParameters;code;codeArray;results CSharpCodeProvider← '.net' ⎕new 'Microsoft.CSharp.CSharpCodeProvider' ⍝ Create parameters for compiler loParameters← '.net' ⎕new 'System.CodeDom.Compiler.CompilerParameters' ⊣ loParameters.ReferencedAssemblies.Add 'System.dll' ⊣ loParameters.ReferencedAssemblies.Add 'System.Windows.Forms.dll' loParameters.GenerateInMemory←0 ⍝ Create code to compile code←⎕IMPORT 'C:\Lindoapi\include\lindo.cs' 'txt' ⍝ Convert code to array containing single string codeArray← 'System.String' MakeNetArray ,⊂code ⍝ Compile it results←CSharpCodeProvider.CompileAssemblyFromSource loParameters codeArray ⍝ Any errors? :If 0 ≠ results.Errors.Count :OrIf results.Errors.HasErrors R←⎕NULL :Return :EndIf ⍝ Create an instance of the 'lindo' class R←results.CompiledAssembly.CreateInstance 'lindo'
See here for more information on dynamic compilation of C# code in APLX.
Utility Function
In the following code we need to construct several .NET arrays of various types. The following utility function can help us.
R←type MakeNetArray values;objType;arrayType ⍝ Create a .NET array of the specified type containing the specified values, e.g. ⍝ ⍝ 'System.Double' MakeNetArray 1.0 2.0 3.0 ⎕IO←0 values←,values ⍝ Create array of correct type and length objType←'.net' ⎕GETCLASS type arrayType←'.net' ⎕GETCLASS 'System.Array' R←arrayType.CreateInstance.⎕REF objType (↑⍴values) ⍝ Fill in values ⊣ R.SetValue ¨⊂[1]values,[0.5]⍳⍴values
Using the LINDO API wrapper
Lindo Systems provides several examples of using the LINDO API .NET wrapper from code written in C#. Here we will take C:\Lindoapi\samples\dotnet\cs\ex_lp1\ex_lp1.cs and present a rough APL equivalent. Note that it doesn't provide a callback function, unlike the C# version. This is currently not possible in APLX, but should not be a problem here because LINDO API callbacks are mainly used for progress reporting.
Example1;⎕IO;nVars;nCons;lindo;LicenseKey;nErrorCode;string;pEnv;pModel;nDir;dObjConst;adC;adB;acConTypes;nNZ;anBegCol;pnLenCol;adA;anRowX;pdLower;pdUpper;varnames;connames;method;nSolStatus;adX;adR;adS;adY;namebuf;i
⍝
⍝ Example of calling Lindo API from APLX
⍝ Example based on C:\Lindoapi\samples\dotnet\cs\ex_lp1\ex_lp1.cs
⍝
⎕IO←0
⊣ '.net' ⎕SETUP 'byref' 0
⍝ Number of constraints
nCons←3
⍝ Number of variables
nVars←2
⍝ Create instance of Lindo class
⍝ The method of doing this depends on how you compile the C# wrapper
⍝
⍝ Method1: Wrapper compiled into class library by Visual Studio
⍝ lindo←'.net' ⎕new 'lindo'
⍝
⍝ Method2: Compile wrapper dynamically
⍝ lindo←CompileLindo
⍝
lindo←'.net' ⎕new 'lindo'
⍝ >>> Step 1 <<< Create a LINDO environment
LicenseKey←'.net' ⎕NEW 'StringBuilder' lindo.LS_MAX_ERROR_MESSAGE_LENGTH
nErrorCode←lindo.LSloadLicenseString 'C:\Lindoapi\license\lndapi60.lic' LicenseKey
string←LicenseKey.ToString
⊣ '.net' ⎕SETUP 'byref' 1
pEnv←lindo.LScreateEnv nErrorCode string
nErrorCode←↑ '.net' ⎕setup 'args'
⊣ '.net' ⎕SETUP 'byref' 0
⍝ >>> Step 2 <<< Create a model in the environment
⊣ '.net' ⎕SETUP 'byref' 1
pModel←lindo.LScreateModel pEnv nErrorCode
nErrorCode←↑ ¯1↑'.net' ⎕setup 'args'
⊣'.net' ⎕SETUP 'byref' 0
⍝ >>> Step 3 <<< Specify the model
⍝ The direction of optimization
nDir ← lindo.LS_MAX
⍝ The objective's constant term
dObjConst←0.0
⍝ The coefficients of the objective function
adC←'System.Double' MakeNetArray 20.0 30.0
⍝ The right-hand sides of the constraints
adB←'System.Double' MakeNetArray 120.0, 60.0, 50.0
⍝ The constraint types
acConTypes←'LLL'
⍝ The number of nonzeros in the constraint matrix
nNZ←4
⍝ The indices of the first nonzero in each column
anBegCol←'System.Int32' MakeNetArray 0 2 nNZ
⍝ The length of each column
pnLenCol←'System.Int32' MakeNetArray 2 2
⍝The nonzero coefficients
adA←'System.Double' MakeNetArray 1.0, 1.0, 2.0, 1.0
⍝ The row indices of the nonzero coefficients
anRowX←'System.Int32' MakeNetArray 0 1 0 2
⍝ Simple upper and lower bounds on the variables
pdLower←'System.Double' MakeNetArray 0 0
pdUpper←'System.Double' MakeNetArray lindo.LS_INFINITY,lindo.LS_INFINITY
varnames←'System.String' MakeNetArray 'Variable1' 'Variable2'
connames←'System.String' MakeNetArray 'Constraint1' 'Constraint2' 'Constraint3'
⍝ We have now assembled a full description of the model.
⍝ We pass this information to LSloadLPData with the
⍝ following call
nErrorCode←lindo.LSloadLPData pModel nCons nVars nDir dObjConst adC adB acConTypes nNZ anBegCol pnLenCol adA anRowX pdLower pdUpper
nErrorCode←lindo.LSloadNameData pModel 'MyTitle' 'MyObj' '' '' '' connames varnames ('System.String' MakeNetArray '')
⍝ >>> Step 4 <<< Perform the optimization
method←lindo.LS_METHOD_PSIMPLEX
⊣ '.net' ⎕SETUP 'byref' 1
nSolStatus←0
nErrorCode←lindo.LSoptimize pModel method nSolStatus
nSolStatus←↑¯1↑'.net' ⎕SETUP 'args'
⊣ '.net' ⎕SETUP 'byref' 0
⍝ Step 5 <<< Retrieve the solution
adX←'System.Double' MakeNetArray 0 0
adR←'System.Double' MakeNetArray 0 0
adS←'System.Double' MakeNetArray 0 0 0
adY←'System.Double' MakeNetArray 0 0 0
⍝ Get the variable values
nErrorCode←lindo.LSgetPrimalSolution pModel adX
⍝ Get the slack values
nErrorCode←lindo.LSgetSlacks pModel adS
⍝ Get the variable values
nErrorCode←lindo.LSgetDualSolution pModel adY
⍝ Get the slack values
nErrorCode←lindo.LSgetReducedCosts pModel adR
'Primal solution'
namebuf←'.net' ⎕new 'StringBuilder' lindo.LS_MAX_ERROR_MESSAGE_LENGTH
:For i :In ⍳nVars
nErrorCode ← lindo.LSgetVariableNamej pModel i namebuf
namebuf.ToString, adX.⎕val[i], adR.⎕val[i]
:EndFor
''
'Dual solution'
:For i :In ⍳nCons
nErrorCode ← lindo.LSgetConstraintNamei pModel i namebuf
namebuf.ToString, adY.⎕val[i], adS.⎕val[i]
:EndFor
⍝ >>> Step 6 <<< Delete the LINDO environment
⊣ '.net' ⎕SETUP 'byref' 1
nErrorCode ← lindo.LSdeleteModel pModel
nErrorCode ← lindo.LSdeleteEnv pEnv
⊣ '.net' ⎕SETUP 'byref' 0
Output of the function
Here's the output of the function
Example1 Primal solution Variable1 60 0 Variable2 30 0 Dual solution Constraint1 15 0 Constraint2 5 0 Constraint3 0 20
An explanation of ⎕SETUP 'byref'
Most of the code in the example above is a straight translation from Lindo's C# example into the APLX equivalent. If you compare the two, the correspondence should be clear.
However there's one step that may need explaining, which is the way APLX handles the C# keywords 'ref' and 'out'. These are used in C# to specify that a parameter is both an input and an output to a function (ref), or is only an output (out). For example, in the C# call:
pModel = lindo.LScreateModel ( pEnv, ref nErrorCode);
...the explicit result is assigned to pModel, but an additional result is returned in nErrorCode.
APLX uses a pass-by-value calling convention, and functions can only have an explicit result. In order to call the LScreateModel routine, APLX needs to be told to use a different calling convention for .NET by using ⎕SETUP 'byref' 1
⊣ '.net' ⎕SETUP 'byref' 1 pModel←lindo.LScreateModel pEnv nErrorCode
After the call the function arguments are available by calling ⎕setup 'args'. Note that this returns a list of all the arguments, modified or not. We can pick off the nErrorCode from the end:
nErrorCode←↑ ¯1↑'.net' ⎕setup 'args'
Finally we need to restore the normal pass-by-value calling convention so future .NET calls work as expected:
⊣'.net' ⎕SETUP 'byref' 0
Sample Workspace
A sample workspace containing the code above is attached to this Wiki page
Author: SimonMarsden
APL Wiki