Implementation Notes for lib(eplex), in ECLiPSe 5.6
Author: Joachim, Kish
Date: July 2003
Revision history: July 2001 (original),
May 2002 (modified for 5.4), Dec 2002 (modified for 5.5)
Contents:
#Global structure
#Eclipse Code Level
#C Code Level
Global structure
These notes applies to the original eplex only. From ECLiPSe version
5.6, the standalone version of eplex, which does not require an ECLiPSe
bounds keeper (ic or range) to keep the bounds of the variables, was
introduced. This is described separately.
Files
Components:
- eplex.c: low level functions, lots of ifdefs for CPLEX
and XPRESS and different versions of them
- eplex.pl: top-level wrapper for loading the eplex library
(with either range, ic or the external solver (standalone) for the
interval).
- eplex_with_ic.ecl and eplex_with_range.ecl: provides the
ic- or range- specific macros and support. One of these are
loaded by eplex.pl. In turn it loads eplex_.ecl, where the bulk of the
ECLiPSe code for eplex is. There are two modulles, eplex and eplex_.
- eplex_.ecl: generic (independent of cplex/xpress,
ic/range) code implementing the library functionality. This code is
loaded into the eplex_ module.
- ic_eplex.ecl and range_eplex.ecl:
trivial wrappers for eplex.pl to force loading of either ic-
or range- for the interval.
- eplex_xpress.pl and eplex_cplex.pl: trivial wrappers for
eplex.pl to force loading of either solver
- eplex_comments: the comment directives documentation for
eplex.
- eplex_lic_info.pl: data file that can be edited by the
user (which licence on which machine). Also determines the default
solver.
- empty_language.ecl: contains the small set of predicates
that are needed in the dummy modules used to select external
solver/bounds keeper.
- eplex_params.h: contains a C table to map the
CPLEX/XPRESS parameter value to symbolic names used on the Eclipse
level. This is created semi-automatically from the xpresso.h or cplex.h
respectively.
- eplex_xpress.def and eplex_cplex.def: DLL export
specification for the Windows build
- WinMSC/EplexXpress and WinMSC/EplexCplex: the
corresponding projects for the Windows build
- bugxps.h, bugxp14.h, bugcpx.h: the include files
for the logging code
Related:
- fdplex.pl: a sample fd/eplex hybrid solver
- mip.pl: a mip branch-and-bound on top of eplex
- mipex.pl: some tests, not in the distribution
- linearize.pl: used to linearize arithmetic expressions
- lib(ic): provides the interval-variables used in eplex
(alternative: range.pl)
- lib(range): provides the range-variables used in eplex
(alternative:ic.pl)
- lib(var_name): provides stable variable name support
- lib(constraint_pools): support for eplex instances
- lib(colgen): column generation library, which uses
lib(eplex)
- lib(bfs): best-first search library, can be used in conjunction
with lib(colgen)
Modules
eplex.pl contains two modules eplex and eplex_. Most of
the code is in eplex_ and reexported through eplex. The reason to have
eplex_ at all is that eplex redefines the arithmetic predicates (>,
>=, ...) which would be inconvenient to have in the actual
implementation module.
There are four dummy modules, eplex_cplex and eplex_xpress
, ic_eplex and range_eplex (defined in the
corresponding files). They don't contain anything. In addition,
to avoid the confusion of apparently being able to post arithematic
constraints to these module, they only reexport the small subset of
eclipse_language built-ins needed by the small amount of code in these
modules.The only effect is that eplex.pl loads eplex_with_range.ecl
when range_eplex exists, and the ic_with_range.ecl when ic_eplex
exists; and simularily, eplex_.ecl attempts to load the Cplex version
when the module eplex_cplex exists, and the Xpress version when module
eplex_xpress exists. If none of them exists, the loaded solver is
solely determined by the eplex_lic_info file.
Debugging
The C code is littered (and made even more unreadable) by macros which
can be used to produce a trace of all XPRESS/CPLEX functions that get
called. This trace can be turned into a pure C bug report, so that any
external solver bugs can be isolated from ECLiPSe. See section on logging.
Eclipse Code Level
Licences and versions
Quite a bit of code at the beginning of eplex_.ecl deals with finding
the right version of the C code to load and obtaining a licence
(because the underlying solver is usually licence-protected). We use a
little database file that tells the library which solver and version to
use on a particular host, and where to find the licence for this
solver. The information is held in the file eplex_lic_info.ecl
in the Eclipse lib directory. If both Cplex and Xpress are available on
a particular host, the selected solver is
- Cplex, if lib(eplex_cplex) was called
- Xpress, if lib(eplex_xpress) was called
- the first one listed in eplex_lic_info.ecl if lib(eplex)
was called
It is not possible to have two different external solvers in the same
Eclipse session.
The pair of predicates lp_get_licence/0 and lp_release_licence/0 obtain
or release a licence. lp_get_licence/0 fails if no licence is availble,
but it can be called again until one becomes available. On loading, the
library tries to grab a licence, and prints a warning if this is not
possible.
We also have OEM versions of Xpress that can be used with ECLiPSe
which the user can use without obtaining an Xpress license from Dash.
This checking is done at the C level (in p_cpx_init()) if the macro
XPRESS_OEM_ICPARC_2002 is defined. It is done in C to avoid checking
for 32 bit overflow: the formular used by Dash requires 32 bit integers
to work correctly, whereas in ECLiPSe, integers > 32 bits will
become bignums. Different keys are needed for the different versions of
Xpress, and this is hard-coded into the C code via macros.
Attribute
Eplex variables have the following attribute:
:- export struct(
eplex(
solver, % A solver that this variable occurs in
idx, % The column number in that solver's matrix
intol_inst, % Suspension list to be woken on intolerable
next % can point to a different eplex attribute for another
% handler, or the atom 'end'
)
).
The eplex attribute consists of a chain of one or more attribute
structures. Each attribute structure belongs to a specific solver
state. Together they represent the eplex problems the particular
variable occurs in.
The solver field contains one of the problem handles
where the variable occurs, and the corresponding column index idx.
This information is mainly needed in lp_var_get/4 to retrieve
variable-related information such as the lp solution. This link is the
main problem when we consider dealing with multiple solver handles
simultaneously.
The intol_inst waking list is only used for a very special
purpose, namely waking when the variable gets instantiated to a value
that is outside of a certain tolerance from the lp solution. The
tolerances can be set by using lp_set/3 on the solver handle and are
different for continuous and integer variables.
The next field points to the next attribute structure, or the
atom end if there are none. An atom instead of a variable is used
to terminate the chain to avoid potential problems with using setarg
(setarg needs to be used as the chain needs to be modified in cases
like when an attribute is discarded (see unification of attribute
chains below)). Each attribute structure in a chain must refer
to a different solver state. This assumption is made in
many of the routines that handle the attribute chains.
:- meta_attribute(eplex, [
print:lp_var_print/2,
unify:unify_eplex/2,
suspensions:suspensions_eplex/3
]).
In general, the attribute handlers have to handle every single
attribute structure in the chain. The attribute print handler prints
the lp solutions of all the attribute structures in the
chain(using the solver/idx fields in the attribute). The unify handler
is responsible for:
- schedulting the intol_inst list if necessary
- merging the eplex attribute chains when two eplex variables are
unified:
- transfer the attribute chain if necessary if only one variable
is an eplex variable.
- if both are eplex variables, their chain is merged. The chain
needs to be checked to see that the two chains do not contain
attributes structures for the same solver state. If they do, one of the
attribute structure is discarded, and an equality constraint send to
the solver state. The event lp_unify_same_handle is also
raised.
Problem Handle
What we call a problem in the following consists of
- a set of constraints
- an objective function
- the variables that occur in these constraints or the objective
function
- a (possibly empty) subset of these variables that are considered
integers
- a number of option settings
Multiple problems are supported by eplex, each problem having its own
handles.We have problem handles on several levels:
- Eclipse level: this is an Eclipse structure (struct prob
), its fields are backtracked, the first field refers to the C level
descriptor
- C level: this is a C structure (struct lp_desc),
its fields are non-backtracked, it refers among others to the solver
state descriptor
- CPLEX/XPRESS level: this is the solver's problem handle
(if any) which is a black box for us. We refer to this as the solver
state.
The Eclipse handle contains all the information we want to hold
on the Eclipse level, for whatever reason (because it it more
convenient, because it is logical storage, etc). Among the fields are:
:- export struct(
prob(
cplex_handle, % int: pointer to C data structure
...
vars, % ''(X1,...,Xn): struct of problem variables
ints, % [Xi1,...Xik]: list of integer variables
...
method, % atom: solving method (primal, dual, ...)
demon_tol_int, % float: tolerable deviation of instantiation
demon_tol_real, % float: ... to simplex solution.
...
presolve, % 1 for yes, 0 for no
use_copy, % 1 for yes, 0 for no
...
status, % return status of last successful invocation
cost, % float: cost of the current solution
sols, % array[n] of raw solutions (or [] or _)
pis, % array[m] of dual values (or [] or _)
slacks, % array[m] of slacks (or [] or _)
djs, % array[n] of reduced costs (or [] or _)
base % iarray[n+m] of basis status (or [] or _)
)
).
The vars array keeps a reference to all the variables involved
in the problem. The array index corresponds to the column number on the
solver level (except that Eclipse array indixes run from 1..N and
column number from 0..N-1). The variables that are treated as integers
by the solver are stored in an additional list ints. If this
list is empty, we only solve LP problems!
The presolve field
specifies the presolve setting for this problem. If the solver supports
only global parameter, then this local setting for presolve is simulated
at the C-level by changing the parameter whenever it is needed for the
problem. In addition, some solvers may have more than a simple binary
setting for the presolve, and indeed different parameters for the MIP
and LP presolves (Xpress). In this case, the presolve setting maps to all
problem types with some default values when presolve is on.
The use_copy field
specifies if a copy of the problem at the C-level should be used when a
MIP problem is solved. Using a copy allows a MIP problem to be modified
when Xpress is used; as otherwise Xpress does not allow a MIP
problem to be modified (beyond changing bounds) once the MIP search has
started.
The method and demon_tol fields store control
parameters for the solver (others are stored on a lower level).
The remaining group are solver results: status is an integer
that contains the underlying solver's last return status (not normally
needed by the programmer). cost is the cost of the last solution
that was computed.
The subsequent arrays contain the result values belonging to
that solution. Note that not all these arrays need to be in use. When
they are initialised to free variables, they remain unused. When they
are initialised to nil, they will be setarg'd to new arrays after every
successful run of the solver. This initialisation is done by the
corresponding solver options (in lp_setup/3 or later via lp_set/3):
Result option name |
Default setting |
Array name in prob |
solution |
yes |
sols |
dual_solution |
no |
pis |
slack |
no |
slacks |
reduced_cost |
no |
djs |
keep_basis |
no |
base |
Note again that this is logical storage which is properly
reset on backtracking, while everything that is stored at the C level
is not. Where reset is needed at the C level, explcit "undo functions"
needs to be trailed.
Support for column generation
The column generation library uses the eplex library. The
main support provided by eplex for column generation are:
- returning row indicies for the added constraints
- adding columns with coefficients for existing rows, and also a
non-zero objective coefficient
- retreiving column data from the ECLiPSe level
Support for multiple problems
As disccused under the attribute section, multiple problems are
supported in the ECLiPSe level with chains of eplex attributes. A
variable can be involved in multiple eplex problems, each with a
different solver state, and each attribute structure within the eplex
attribute for the variable represents one solver state.
Each ECLiPSe problem handle is given a unique integer id, the solver
id . Each time a solver state is created, it is assigned a new
solver id maintained within a global reference, whose value is
incremented for the next solver id. The solver id is stored into the solver_id
field of the problem handle for the solver state, and is used to
identify the solver state within the ECLiPSe code for the library, for
example, in predicates which needs to access a specific eplex
attribute structure in a chain.
The concept of eplex instances is
used to represent multiple eplex problems at a high-level. This allows
constraints for an eplex problem to be posted before a solver state is
set up for it. Note that the eplex attribute structure is not
created until the solver state is set up.
Managing solver parameters
With Xpress 13+, the solver parameters are specific to each problem and
there are no global parameters. With Cplex and older Xpresses, the
parameters are global (i.e. applies to all problems). This difference
unfortunately cannot be hidden from the user. An additional
complication is that currently a ECLiPSe level problem may not yet have
a C-level solver handle (e.g. if the problem was empty). This is an
issue if the solver parameters are problem specific.
We allow access (get/set) to these parameters via the problem handle
(or eplex instance)[local access], or `globally', without specifying any
handle/instance. The exact meaning of `global' parameter depends on if
the solver has global parameters or not:
- solver has global parameters: acess to global parameters in eplex
directly accesses the corresponding solver parameter.
- solver has no global parameters: `global' parameters are the
default settings for the parameters, i.e. the values that would be
given to a new problem when it is set up (at the C level). It does not affect any existing
problem's parameter.
Thus, the two cases of the global parameters behaves in the same way
only if the parameters are accessed before any problem setup.
The following table shows the value that is returned for various
types of accesses. There are three types of access: global access (no
handle), local access, no C handle (with a problem handle that does not
yet have a low level handle), local access, with C handle (with a
problem handle with a low level handle):
access
type |
solver
has local params |
solver
has global params |
global |
default value |
global value |
local, no C handle |
default value |
global value |
local, with C handle |
problem's value |
global value |
We plan to remove the complication of wheather there is a C level
handle or not by always setting up a low level solver for a problem
during problem setup.
We do not allow the user to set a
solver parameter locally if the solver has global parameters only,
because this would have the `side effect' of changing this parameter
globally.
Also, note that the presolve parameter
is treated specially. This does not correspond directly to the low
level presolve parameter of the solver. Instead it always behaves as if
this parameter is local, i.e. it is specific to each problem, and if
set globally, it sets the default value. With solvers that only has
global parameters, the local behaviour is simulated as discussed here.
The default values for parameters in Xpress 13+ is stored in a dummy
problem which is created on initialisation. The values of these
parameters is copied into each new problem that are created.
When used from ECLiPSe, these parameters needed to be wrapped in a
optimizer_param/1 structure, to clearly show that they are optimizer
(and version) specific. We also provide two special aliases that do not
need the optimizer_param/1 wrapping: presolve,
as discussed above, and timeout.
This allow the user to avoid version specific code for these common
cases, and also not needing to worry about the differences in the
actual solver parameter (different names, different argument type,
different semantics).
Normalised expressions and constraints
All expressions occurring in eplex constraints are turned into
normalised linear form using lib(linearize):
[C0*1, C1*X1, C2*X2, ...]
where Ci are numbers and Xi are distinct variables. The first
(constant) term is always present, Ci (i>=1) are nonzero.
Constraints are normalized by bringing all terms to one side of the
relational symbol and then storing just the relational symbol and the
non-zero side as a normalised expression:
Sense:NormExpr
where Sense is one of the atoms =:=, >= or =< and NormExpr is a
normalised expression as above. E.g.
(>=):[-5*1,3*X]
could encode the source constraint
5 =< X*3
There is also a quadratic normal form being used for quadratic
objectives, see lib(linearize) for details.
Eplex
instances
For ease of programming multiple eplex problems, the abstraction of eplex
instances is introduced. Conceptually, an eplex instance is an
instance of the eplex solver for one eplex problem. Each
eplex instance is a module, and from a user's point of view, it works
like any other solver module: predicates are defined in the module
which allows the user to post linear arithmetic and intergrality
constraints, and to set up and invoke an external solver to solve the
problem. There are three components:
- storage for constraints
- predicates to set up and trigger solver state
- associated external solver state (if one has been setup)
Storage for constraints
This is implemented using lib(constraint_pools). The posted constraints
are buffered on the ECLiPSe level in constraint pools, one pool per
eplex instance.
When an eplex instance is declared using eplex_instance/1, an eplex
instance is either created (if it does not already exist) or checked to
see if it is empty (no posted constraints in the constraint pool, no
associated solver). An eplex instance is created by creating the
constraint pool for it.
The constraints are divided into two types, defined in the structure
constraint_type in eplex_.ecl:
:- local struct(constraint_type(integers,linear)).
integers are the intergrality constraints (integers/1), and linear
are the linear arithmetic constraints (=:=/2, >=/2, =</2).
When a linear constraint is posted, the
following happens:
1. the constraint gets normalised
2. if ground, solve it now, i.e. check if satisfied
3. if it has only one variable, turn it into a (range- or
ic-) bound update
4. otherwise, add the normalised form to the linear
constraint type of the constraint pool.
integers/1 is handled by
- imposing ic:integers/1 or range:integers/1 on the variables
- eliminating ground integers from the list
- add the remaining variables to the integers constraint type of
the constraint pool. For efficiency, when the integers list is
traversed to eliminate ground integers, a unbounded tail is created to
allow the existing integers to be added to this tail
Thus the constraints are not actually maintained as suspensions, but
logically they are still part of the resolvent. Any constraints that
remain in any of the eplex instances' constraint pools are thus printed
out at the end of an execution. This is done by calling
set_lp_pending/0 whenever a constraint is added to an eplex instance.
set_lp_pending/0 will create an lp_pending/0 suspension if one does not
already exist, so the presence of this suspended goal indicates that
there are possibly some constraints posted to some eplex instance. A
portray macro for lp_pending/0 is defined to print out the outstanding
constraints in all the eplex instances, so that when the delay goals
are printed at the end of an execution, the eplex constraints would be
printed instead of lp_pending/0. Note that constraints collected by an
external solver will not be printed, even if the external solver was
not invoked to solve the problem.
The suspended lp_pending goal is stored in the global reference lp_info
in the module eplex_. lp_info is a structure with two fields:
:- local struct(lp_info(newid, % int: next
handler id
pending % suspension: lp_pending's suspension
)).
newid is the next solver id.
Predicates for the eplex instance
Predicates defined in the eplex instances simply call their
counter-parts in the module eplex_ with the eplex instance name as an
argument. All the real work is done by the predicates in the eplex_
module. This is specified in the create_constraint_pool/3 call
when the constraint pool is created.
Except for the constraint predicates, all other predicate names
defined for an eplex instance start with eplex_. The predicates
(in the eplex instance) are:
constraints:
integers/1, =:=/2, >=/2, =</2
solver setup:
eplex_solver_setup/1, eplex_solver_setup/5
solver invocation: eplex_solve/1,
eplex_probe/2
solver state:
eplex_get/2, eplex_var_get/3
destroy solver:
eplex_cleanup/0
read/write problem:
eplex_read/2, eplex_write/2
In turn, the eplex_* predicates in eplex_.ecl calls the low-level
predicate that uses solver state handles. The solver handle is for the
solver state that is associated with the particular eplex instance.
Associating external solver state
A new external solver state can be associated with an eplex instance by
the solver setup predicates of the eplex instance. The solver state is
first created and then associated with the eplex instance.
Associating with an eplex instance is done using the pool item
facility of lib(constraint_pools). The ECLiPSe handle for a solver
state is stored in the eplex instance's pool item if the eplex instance
has an associated solver, otherwise 0 is stored as the pool item.
lp_setup
This is really the core of the library. It takes as input
- a list of already normalised constraints (equalities and
inequalities)
- an objective expression (linear or quadratic)
- a list of integer variables (within the option list)
- a list of solver options
This data is used to set up a linear, quadratic or mixed integer
problem with the external solver, and a handle to it is returned.
- A quadratic problem (qp) is set up if the objective is not linear
- A mixed integer problem (mip) is set up if the list of integers
is not empty. Note that the integrality-information in the
range-attribute (range:integers/1) is completely ignored for this
purpose. This allows lp_setup to set up a linear relaxation easily.
- A linear problem (lp) is set up otherwise
There are a few interdependencies
- there are restrictions on changing the problem type, some of
which is imposed by our interface.:
- An lp problem can change to a mip problem, but not vice
versa (except during backtracking) as contraints cannot be removed in
forward execution.
- A qp problem cannot change to a qpmip problem. We have
not yet implemented the interface for dealing qpmip problems.
- A linear problem cannot change to a quadratic problem. This
affects lp_probe only, as this is the only way the objective can be
changed in eplex. There is probably no implementational reason
that this can't be done (currently a qp problem is first set up as an
lp problem at the C level).
- mip problems cannot be quadratic
- quadratic problems imply the use of the barrier method
Pseudocode:
lp_setup:
assign a new solver id
renormalise constraints (possibly do some simplification)
normalise the objective (and determine whether it's linear)
create array of problem variables
create list of integer variables if required
assign column numbers to the variables and store them in the attributes
convert constraints into column-wise lists of row:coefficient pairs
transfer data into the C-level descriptor (including transfer of all variable
bounds from bounds keeper to solver)
call the C-level problem setup function cplex_loadprob()
Note that special treatment is needed for extreme cases (no variables,
no constraints), because the low-level code cannot cope with degenerate
cases.
Each solver state is assigned an unique integer solver id. The next
id to assign is maintained in a global reference.
lp_solve
Takes a problem handle and invokes the actual optimizer. Because it can
be called repeatedly on the same problem handle, it takes care of first
transferring possibly updated variable bounds to the solver,and
optionally loading a basis that might have been saved from a previous
solver run. It may alsoPseudocode:
lp_solve:
setup problem if not already setup
if not a freshly set up problem:
transfer current variable bounds to the solver (all or some)
if a previous basis is available:
transfer basis to the solver
invoke the solver
interpret the result
if successful:
retrieve the cost
retrieve the required result arrays (solutions, basis, etc)
You can see that the updating of variable bounds is not very
well optimized: we normally assume that every variable could have
changed bounds and traverse the whole variable array and transfer all
the current bounds to the solver in case they have changed. Any better
strategy would require to monitor variables individually, and it is not
clear whether this is worthwhile.
About handling the basis: storing the basis at the logical
Eclipse-level is optional. If it is done, then the solver can restart
from the basis of the logical ancestor in the search tree. If it is not
done, it will just use the basis from the chronologically last solver
invocation. The latter is less predicatable, but has less overhead.
Cplex and Xpress can return lots of different result codes. We
classify them into the following groups:
Result |
description |
action |
default handler |
DESCR_SOLVED_SOL |
an optimal solution was found |
success |
success |
DESCR_SOLVED_NOSOL |
no solution was found, infeasible |
fail |
fail |
DESCR_ABORTED_SOL |
a possibly suboptimal solution was found (mip) |
event(eplex_suboptimal) |
success |
DESCR_ABORTED_NOSOL |
no solution was found, but there may be one (mip) |
event(eplex_abort) |
abort |
DESCR_UNBOUNDED_NOSOL |
problem unbounded, no solution values |
event(eplex_unbounded) |
success |
DESCR_UNKNOWN_NOSOL |
unknown whether infeasible or unbounded |
event(eplex_unknown) |
fail |
The infeasible or unbounded result can arise from the use of
presolving or the dual simplex method (because infeasibility of the dual
indicates infeasibility or unboundedness of the primal). The events have
been introduced to enable the library user to tailor the behaviour of
the solver for the doubtful results.
optimize
Optimize (the black-box solver) is a simple sequence of
- collecting the pending constraints (equalities, inequalities and
eplex:integers/1)
- setting up a solver
- run the solver
- unifying the resulting cost
- retrieving the solutions and unifying them with the variables
- cleaning up the solver
lp_demon
For the demon solver, the setup and initial solving is like for the
black-box solver. The differerences are that a demon with appropriate
waking conditions is set up as well, the Cost variable is only bounded
by the solution (rather than unified), and the problem variables are
not touched after solving:
- collecting the pending constraints (equalities, inequalities and
integers/1) from the eplex instance that the demon is associated with
(if any), specified by the collect_from option.
- setting up a solver
- create the lp_demon suspension
- insert the suspension into the suspend field of the handle
- run the solver once if initial_solve option is yes
- calling the post-goal (see user documentation)
- bounding the Cost variable
When the lp_demon is woken subsequently, it performs:
- collecting new pending constraints (equalities, inequalities and
eplex:integers/1) if the solver state is associated with an eplex
instance.
- adding them to the existing handle
- in case new variables have been introduced, let the lp_demon
suspend on them as well
- calling the pre-goal
- run the solver again
- calling the post-goal
- bounding the Cost variable
Triggering the demon:
Trigger condition |
Implementation |
inst |
insert in all variable's (inst of suspend) lists |
bounds |
insert in all variable's (wake_lo/wake_hi of range) lists |
deviating_inst |
insert in all variable's (intol_inst of eplex) lists |
deviating_bounds |
like deviating_inst, additionally set up a
deviating_bounds_demon on every variable which wakes on bound changes
and schedules the intol_inst list when the new bounds exclude the
lp-solution |
Attribute:Index |
insert in all variables' suspension list found
in attribute Attribute in the index'th position |
new_constraint |
the nc_trigger field of the solver state's handle is set to
yes (it defaults to no). When constraints are added to the solver state
(see lp_add below), the nc_trigger filed is checked, and if it is yes,
then the demon is triggered (by scheduling the suspension for the demon
in the suspension field for the handle) |
trigger(atom) |
attach to a global trigger |
lp_add
lp_add add constraints and variables to a previously set up problem
handle. The handles on all levels must be adjusted accordingly. In
general, the problem type can change from lp to mip (if interger
constraints are added to an lp problem). Both linear and integers
constraints can be added (if allowed by the problem type).
Currently we do not support the changing of a qp problem to qmip
problem.
lp_add can be called in several ways, either explicitly (e.g. via
lp_add_constraints), or implicitly (when a demon solver is invoked),
with various steps, which are carried out by three predicates:
- renormalise_and_check_simple:
Renormalise any new linear constraints
- lp_add_normalised: Add
the new (normalised) constraints/variables to the problem
- var_triggers: Add the new
variables to the triggers
Not all calls to adding constraints/variables to the problem perform
all three steps. For example, adding columns for column generation does
not normalise the constraint (because simple constraints has to be
added to the problem).
renormalise_and_check_simple
Renormalised and any ground linear constraints (all variables in
constraint instantiated) are checked for satisfaction and filtered out
(not passed to external solver). Note that unlike when linear
constraints are posted, single variable constraints are not turned into
bound updates.
lp_add_normalised
This predicate returns the row indicies of the added constraints.
This is needed by the column geneation library. In other cases, these
indcies are ignored.
Variables occurs in both the new linear and integers constraints
being added. They can be classified as:
- old variables: variables that are already in the solver
state
- old integer variables: old variables which occur in the
new integers constraints. They may or may not already be constrained to
be integer
- other old variables: old variables which occurs in the
new linear constraints only
- new variables: variables that are not yet in the solver state
- new integer variables: new variables which occur in both the
new linear constraints and integers constraints.
- new non-integer variables: new variables which occur in the new
linear constraints only
- non-problem variables: these are variables are neither in the
solver state or occur in the new linear constraints (i.e. they occur
only in the new integers constraints)
The steps performed by lp_add are:
- If problem not yet initialised (no variables/constraints set up
yet), initialise it
- Classify the variables that occur in the new constraints (both
linear and integers) into old integers, other old variables, new
integers, non-problem variables and new non-integer variables. These
need to be treated differently:
- other old variables: nothing needs to be done with these
- old integers: the type of columns representing these variables
in the external solver matrix need to be changed if it was not
already an integer (or boolean). The type changes are undone on
backtracking at the C level.
- new integers: new column needs to be added to the external
solver matrix, as type integer
- non-problem variables: these are not passed to the external
solver. A warning is printed.
- new non-integer variables: these are new variables that
are not constrained to be integers. A new column needs to be added to
the external solver matrix, as type continueous.
- The C level code are called to possibly add new rows (if there
are new linear constraints) and columns (if there are new problem
variables) with the correct type. (cplex_flush_new_rows)
- If var_names option is set, pass any variable names for the new
varaibles to the solver state.
var_triggers
If there are variable trigger conditions, add the variable trigger
condition(s) to the new variables. This same code is used here as
during the demon solver's setup.
lp_add_constraints
This basically calls lp_add after normalising the constraints, and then
filter out simple one variable linear constraints as done when linear
constraints are posted to an eplex instance.
After calling lp_add, the solver is invoked if the nc_trigger field of
the handle is set to yes.
lp_eq, lp_ge, lp_le
These are called when the linear constraints (=:=, >=, =< and
their $ equivalents, respectively) are posted to an eplex
instance. The actiions performed are described here.
lp_probe
lp_probe is simply a wrapper around lp_solve:
lp_probe:
change Handle's objective function to temporary one
invoke lp_solve on Handle
change Handle's objective function back to the original one
if a variable occurs in the new objective, but not in the problem, it
is added to the problem if it has bounds of the bounds keeper (i.e.
range for range_eplex, ic for ic_eplex). This is to get around the
problem where the user post only simple bounds constraints for such
variable, which are then not added to the problem. This is not a
problem for stand-alone eplex, as such variables are added to the
problem.
lp_add_columns
adds new columns to the external solver matrix. Unlike lp_add, these
new columns may be non-empty, i.e. there may be non-zero values for the
existing rows, and objective coefficient.
This is used by the column generation library to add the generated
columns. The column information is specified by a list [Var, Obj, Col],
where Var is the variable representing the new column, Obj is its
objective coefficient, and Col is a list of RowIdx-Value pair
representing the non-zero column coefficients.
The steps performed by lp_add_columns are (code is based on the
lp_add code, with column-wise C-level buffer arrays for the column
coefficients (cmatbeg, cmatind,cmatval) in the problem descriptor:
- extract the individual column data for the new columns, filtering
out existing columns
- set the new indecies for the new columns and update the buffer
arrays (column coefficients and bounds)
- flush the data to the external solver state by calling
cplex_flush_new_cols
- set the var_names for the new columns (if required)
- set the objective coefficients for the new columns (earlier
versions of this code clears all the objective coefficients, including
the existing one, and resets everything).
- extend the basis by retaining the last basis and giving 0 to the
new columns
reduced_cost_pruning
reduced _cost_pruning implements an optional functionality that the
programmer can invoke on a solved handle (see for example Michela
Manela's work). If we are solving the lp-relaxation of a more complex
global problem, and we have a maximum (minimum) of the relaxation, and
a lower (upper) bound on the global problem, then we can use the
reduced cost information of the relaxed solution to prune variable
values that cannot possibly yield a better solution to the global
problem.
For those variables whose solution value is at one of their bounds, the
reduced cost gives a gradient which says how much the objective
deteriorates (at least) when the value moves away from the bound.
Values that are so far away that the relaxed cost gets worse than the
known bound on the global cost, are useless and so the opposite bound
may be pruned to that value.
C Code Level
Versions
The C code needs to be compiled separately for every version of the
Cplex or Xpress solver that we want to support. The makefile builds
differently named shared libraries from the single eplex.c source file,
e.g.
- ecplex65.so for Cplex 6.5 (compiled with -DCPLEX=6 and -D
CPLEXMINOR=5)
- express1412.so for Xpress 14.12 (compiled with -DXPRESS=14)
One Eclipse distribution can contain several versions of the compiled C
code for different solvers and different solver versions. At runtime
however, when lib(eplex) is loaded, it can only load one of them. Which
one gets loaded depends on the information in the eplex_lic_info.ecl
file, see the loader code at the beginning of eplex.pl.
The C code links in the external solver library code statically if
possible, and is itself dynamically loaded when lib(eplex) is loaded.
However, static linking is not possible with Xpress 14, and in addition
two dynamic libraries: the solver itself, and the licensing code, have
to be loaded. In addition, the files must be loaded with their full
name, including the minor version numbers at the end (e.g.
libxprl.so.1.0.3), as this is checked in some systems. To make our code
as generic as possible, we ensure that only one copy of each dynamic
library is copied into the right subdirectory and preload (in ECLiPSe
code) the library by specifying the file name partially (e.g.
libxprl.so.* ).
Problem descriptor
We maintain a C-level problem descriptor (which is itself referred to
by the Prolog level problem handle)
This descriptor stores:
- the solver-specific problem descriptor (CPXLPptr for Cplex, or
XPRSprob for Xpress 13+)
- additional data about the problem we want to maintain in
non-backtracked storage (e.g. success and failure counters)
- all data (esp. arrays) that need to be built up incrementally
during problem setup, and that are then used as input to the solver's
own problem setup function
- some problem specific settings that are more convenient/efficient
to maintain at the C level, e.g. the presolve state
The descriptor has a state which is either one of the solution states
described under lp_solve/2, or
- DESCR_LOADED during setup, no solve attempted yet
- DESCR_EMPTY after cleanup
Initialization and finalization
Two C externals are provided for initialise and finalise the whole
subsystem, essentially this means dealing with the licences:
- cplex_init(LicenceLocation, SerialNum), p_cpx_init
tries to obtain a solver licence, fails if this is not possible. The
arguments come from the eplex_lic_info.ecl file. SerialNum is only
relevant for Cplex runtime licences. - cplex_exit, p_cpx_exit
releases the solver licence, and deallocation of some of the storage
Problem name
Xpress requires a problem name when initialising. Moreover, the problem
name is used to generate various intermediate results files (mostly
during MIP search). If more than one Xpress is running at the same time
within the same file space, then name clashes are possible which might
lead to incorrect behaviour. In order to avoid this, the problem name
is generated from the machine's host id and the current process's id.
For Xpress, the problem name can contain directory paths, so that
the intermediate files can be written in other directory than the
current directory. To allow for this, we allow the user to
specifiy a "tem_dir" option for the path. This can have a large impact
on performance (elasped time), because the accessing the current
directory can be slow (e.g. a NFS file store, like our local file
space), and in addition this will allow the user to run their program
in a directory they do not have write permission for.
Problem setup and cleanup
Problem setup requires calling many small C externals from the Eclipse
code. It is done is three steps. First, cplex_prob_init/8 allocates a C
level descriptor including sufficiently large arrays. These arrays are
the filled successively using the cplex_set_xxx or cplex_loadxxx
functions. When this is finished, cplex_loadprob calls the actual
Cplex/Xpress setup routine, passing the previously prepared arrays. In
the following, CPH stands for the C level handle, I stands for row
numbers, J for column numbers:
- cplex_prob_init(PreSolve,UseCopy,Rows,Cols,NonZeros,ExtraRows,ExtraCols,ExtraNz,Sense,-Handle),
p_cpx_prob_init
allocates the C-level handle and stores it into the first argument of
the Eclipse-level handle. Also allocates arrays for a complete
column-wise representation of a problem of the given size (number of
rows, columns, nonzeros). An undo-function is trailed which will
deallocate the handle with all its arrays on backtracking or garbage
collection. The ExtraXXX arguments are no longer needed since Xpress
R12. The Presolve argument is either 1 or 0, and the C level
descriptor's presolve field is set to this value. At the external
solver level, the presolve status is global (for Cplex and Xpress), and
before calling any external solver library calls that may be affected
by the presolve state, the presolve state is set according to the
presolve field. This allows the presolve state to be set on a
per-problem basis.
- cplex_set_rhs_coeff(CPH,I,SenseCode,Rhs), p_cpx_set_rhs_coeff
set constraint sense and right hand side for constraint row I. - cplex_set_obj_coeff(CPH,J,Val),
p_cpx_set_obj_coeff
set objective coefficient for variable column J. - cplex_set_qobj_coeff(CPH,J1,J2,Val),
p_cpx_set_qobj_coeff
set quadratic objective coefficient for the product of variables column
J1 and J2 - cplex_set_bounds(CPH,J,Lo,Hi), p_cpx_set_bounds
set bounds of variable column J. - cplex_set_type(CPH,J,TypeCode),
p_cpx_set_type
set type of variable column J. TypeCode is a character C, I or B. - cplex_set_matbeg(CPH,J,K,K1),
p_cpx_set_matbeg
coefficient array locations K to K1-1 contain entries for column J. - cplex_set_matval(CPH,K,I,Cij),
p_cpx_set_matval
set the matrix coefficient for row I and column J (implicit) in
coefficient array location K. - cplex_set_var_name(CPH, Cj,
Name), p_cpx_set_var_name
set the name for the variable column J in the external solver. This
name will be
used
when the external solver prints the problem. - cplex_loadbase(CPH,Basis),
p_cpx_loadbase
load a basis (a string buffer, obtained by retrieving from the handle
earlier) - cplex_loadorder(CPH,Length,OrderList), p_cpx_loadorder
load MIP branching order preferences (undocumented feature) - cplex_loadsos(CPH,SosType,Size,Js),
p_cpx_loadsos
load definition of an SOS 1 or 2. Js is a list of length Size of the
column numbers that make up the SOS. - cplex_loadprob(CPH),
p_cpx_loadprob
set up the actual solver problem using the (now filled) arrays in the C
level handle. - cplex_cleanup(CPH), p_cpx_cleanup
free the problem descriptor (this also happens on untrailing and
garbage collection).
Problem modification
Most of the modification functions are split into the transfer of the
information into growable arrays (hidden on the C level), and a
flush-function which conveys the array contents to the actual solver:
- cplex_new_bounds(CPH,J,Lo,Hi), p_cpx_new_bounds
set new bounds for variable column J - cplex_flush_bounds(CPH),
p_cpx_flush_bounds
actually transfer the new bounds to the solver - cplex_new_obj_coeff(CPH,J,Val),
p_cpx_new_obj_coeff
set new linear objective coefficients - cplex_flush_obj(CPH),
p_cpx_flush_obj
actually transfer new linear objective coefficients to the solver - cplex_new_qobj_coeff(CPH,J1,J2,Val),
p_cpx_new_qobj_coeff
set new quadratic objective coefficients - cplex_new_row(CPH,SenseCode,Rhs,RIdx),
p_cpx_new_row
start adding a new row with given constraint sense and right hand side.
In RIdx the new row's index is returned. - cplex_add_coeff(CPH,J,Cij),
p_cpx_add_coeff
set coefficient for column J in currently added row - cplex_flush_new_rows(Handle),
p_cpx_flush_new_rows
actually transfer one or more new rows and columns to the solver. The
new columns have zero values for all the existing (pre-addition) rows.
Note the Prolog handle is taken rather than CPH; the timestamp in the
Prolog handle is needed for the undo function _cpx_del_rowcols to remove
the added rows and columns.
- cplex_change_type(Handle, J, TypeCode), p_cpx_change_type
change the type of column J to TypeCode (the code for the type obtained
from type_to_cplex_type/4).
- cplex_chg_rhs(CPH, I, Rhs), p_cpx_chg_rhs
change the rhs value for row I to Rhs. - cplex_new_col(CPH),
p_cpx_new_col
start adding a new column. This is used by column generation, and
the column is expected to have non-zero values for the existing rows.
This call is follwed by calls to cplex_add_col_coeff to fill in the
column's non-zero values in the column-wise buffer arrays (cmatval,
cmatind). - cplex_add_col_coeff(CPH, I, Val), p_cpx_add_col_coeff
set the non-zero coefficient for row I to Val in the currently added
column. - cplex_flush_new_cols(CPH), p_cpx_new_col
actually transfer one or more new non-empty (have non-zero values)
columns to the solver. No new rows are added.
For Xpress, only the bounds for a MIP problem can be modified once the
MIP search is started. To get round this restriction, we make a copy of
the problem, which is pointed to by lpcopy, and solve the problem using
the copy so that the original can still be modified. This produces the
problem of :
- The problem pointers lp and lpcopy are always present, even when
it is not needed. In these cases, lpcopy and lp points to the same
problem.
- When copying is needed, the problem is copied from lp to lpcopy
just before it is solved as a MIP problem, and any modifications to the
problem is always done to lp, not lpcopy. Once solved, solution data is
obtained from the copy until it becomes invalid when the problem is
modified (either by forward execution or backtracking in ECLiPSe; the
appropriate function must make the copy invalid by changing the
copystatus, see next item).
- In XPRESS 13+, which needs the problem copy for MIP modification,
an extra field in lp_desc, copystatus,
is used to specify the status of the copy:
- XP_COPYOFF: we are not using a copy (MIP modification would not
be possible, but solving single MIP problems will avoid the copying
overhead)
- XP_COPYINVALID: the problem in lpcopy is invalid.
- XP_COPYCURRENT: the problem in lpcopy is current
The copystatus is used to determine if
information is extracted from lp or lpcopy in procedures that are called
independently from ECLiPSe. Currently this applies only to
p_cpx_lpwrite(). Other accesses to lpcopy are done in code that occurs
in p_cpx_optimise(), after the call to optimise. Here, lpcopy is either
valid and pointing to the problem copy with the result of the
optimisation, or to lp, if copying is not needed (Cplex, or LP/QP in
Xpress).
An LP problem can change into
a MIP problem if integers constraints are added. This may happen after
the problem is initially loaded. This is not a problem for Cplex, which
loads all problems with the same routine. Xpress however, has different
routines for loading different problem types. We get around this by
always using the `global' loading routine (XPRSloadglobal()) for both
LP and MIP problems. If the problem is an LP problem, it is simply
solved as an LP problem in XPRSminim/maxim. This apparently has no
implications in performance for an LP problem. An LP problem can thus
be changed to a MIP problem. On backtracking, this change is undone by
_cpx_reset_mip_to_lp() undo function.
Unfortunately, when Xpress's solving of an LP problem is aborted,
the problem is left in a pre-solved state instead of the original
state. The problem can therefore not be modified correctly
subsequently. No solution to this problem is implemented currently, but
one possible solution is to make copy of an LP problem as well as a
MIP problem.
Problem solving and result retrieval
Running the solver is always done with cplex_optimise, regardless of
the problem type or the solver method used.
- cplex_optimise(+CPH,+Method,-Result,-Status), p_cpx_optimise
run the solver, using the given method (primal,dual,barrier,...). Note
however, that for mip problems the methods only specifies the start
algorithm, and for qp problems the method is always barrier. Result is
the resulting descriptor state (DESCR_XXX) and Status is the underlying
solver's own return code (the latter is never interpreted by the
Eclipse level code, it is just printed or passed to a user error
handler).
There is one function to return the objective value:
- cplex_get_objval(+CPH,-Cost), p_cpx_get_objval
get the cost of the solution found
- get_darray_element(+Darray,+I,-Value),
p_get_darray_element
get element I of a darray
- darray_list(+Darray,-List), p_darray_list
convert a darray to a list of doubles
A darray is actually an Eclipse string, but it contains doubles
rather than characters. We use these as the most efficient way of saving
large floating point result arrays to the Eclipse global stack.
The various results arrays (solution, dual, slacks, reduced costs
and basis) are retrieved inside p_cpx_optimise(). This is done either
by setting up a callback function (Xpress 13+, MIP problems), or
directly after a (successful) optimisation. In Xpress 13+, setting the
callback function avoids Xpress writing solution files during MIP. Note
that some files are still created during the MIP search, however. The
cleanup routines (p_cpx_cleanup()) will delete any such file for the
current session, but if it is not called (e.g. because of some crash),
then extra files may be left behind.
Retrieving problem data
These are non-essential functions, used to retrieve information from
the C level handle:
- cplex_get_prob_param(+CPH,+Which,-Value), p_cpx_get_prob_param
retrieve handle-specific properties (problem type, last status) and
statistics (size, counters) - cplex_get_type_and_bounds(+CPH,+J,-TypeCode,-Lo,-Hi),
p_cpx_get_type_and_bounds
get type and bounds of variable column J - cplex_get_row(+CPH,+I),
p_cpx_get_row
prepare retrieval of row information for row I - cplex_get_rhs(+CPH,+I,-SenseCode,-Rhs),
p_cpx_get_rhs
get current row's sense and right hand side - cplex_get_col_coef(+CPH,+J,-Cij),
p_cpx_get_col_coef
get coefficient of current row, column J - cplex_get_obj_coef(+CPH,+J,-C),
p_cpx_get_obj_coef
get objective coefficient for column J
Other functions
- cplex_get_param(ParamName,Value), p_cpx_get_param
get a global parameter setting. The ParamName is an atom. The result
type is implicit (float,integer,string). This uses the name mapping
table in eplex_params.h - cplex_set_param(ParamName,Value),
p_cpx_set_param
set a global parameter. The ParamName is an atom. The required value
type is determined by the parameter itself. - cplex_lpwrite(File,Format,Handle),
p_cpx_lpwrite
write the problem in a prticular format to File. Cplex supports several
formats, Xpress only 'mps' format. - cplex_lpread(File,Format,Handle),
p_cpx_lpread
read a problem from a file. This only works in Cplex and is neither
widely used nor well tested. - cplex_lo_hi(NegInf,PosInf),
p_cpx_lo_hi
returns the solver's idea of negative and positive infinity (something
like 1e20) - cplex_output_stream(ChannelNr,AddRem,StreamNr),
p_cpx_output_stream
associate or disassociate a Cplex I/O channel (result_channel (0),
error_channel(1), warning_channel(2), log_channel (3)) with the given
Eclipse stream StreamNr. Xpress has a similar concept of 4 message
levels
Eclipse level name |
ChannelNr |
Cplex |
Xpress msg type |
Default Eclipse stream |
result_channel |
0 |
result |
2 |
- |
error_channel |
1 |
error |
4 |
error |
warning_channel |
2 |
warning |
3 |
warning_output |
log_channel |
3 |
log |
1 |
- |
Undo functions
These functions are setup using ec_trail_undo, which places the
function on the trail such that it is called when untrailed.
Additionally, ec_trail_undo allows a time-stamped trailing so that the
function will only be trailed if the time-stamp is `old' (older than
the most recent choicepoint).
- _untrail_cleanup *non
time-stamped*
calls p_cpx_cleanup, and then free the
problem descriptor. Trailed during p_cpx_prob_init so problem space can
be freed upon backtracking
- _cpx_reset_mip_to_lp *non
time-stamped*
reset a problem type from MIP to
LP. CPLEX has to be informed of the changed as well (but not
XPRESS). Trailed when problem is changed from LP to MIP. In future
versions, this function may be extended to change a QPMIP problem back
to a QP problem
- _cpx_reset_col_type *non time-stamped*
change a column back to its original
type. Trailed when a column type is changed in p_cpx_change_type. The
untrail data contains the column's number and the original type. This
function is not time-stamped because each column can only change
type at most twice (C -> I -> B).
- _cpx_del_rowcols *time-stamped*
delete added rows (and any added
columns) from a problem matrix. Rows are always added incrementally, so
only one delete is needed per choice-point even if there were many row
additions. The same time-stamp as _cpx_reset_col_type is used, as there
is no need to reset the types of columns that are deleted. Trailed
during p_cpx_flush_new_rows or p_cpx_flush_new_cols. Untrail data
contains the old column and row sizes.
Multiple handles
While Cplex has always had problem handles, these need to be simulated
for older (pres-13) versions of Xpress. We will not disucss
this further as more recent versions of Xpress also have problem
handles.
Memory allocation
Memory in eplex.c is allocated through the macros Malloc(), Realloc()
and Free(), which are normally set to the standard C library's
allocation functions, but can be redefined for debugging purposes. Note
that we have no control over the memory allocation of the actual solver
library!
Global Parameters
Cplex and Xpress have a large number of parameters that affect their
behaviour. Only a few of them coincide with their meaning. We have
therefore decided to map the parameter access one-to-one from the C
level to the Eclipse level. While the rest of eplex tries hard to hide
all the differences between Cplex and Xpress, the parameter setting is
the exception.
To make things worse, every new release of Cplex or Xpress comes with a
modified set of parameters. We have therefore introduced a fixed
mapping from the parameter names in the solver's C interface to the
(atomic) parameter name that is used on the Eclipse level, e.g.
Cplex's CPX_PARAM_NODELIM becomes nodelim
Xpress's MAXNOD or N_MAXNOD becomes maxnod
The mapping is defined in the file eplex_params.h, which unfortunately
must be updated for every new solver release. The comment at the
beginning of the file explains how to do the job semi-automatically
using editor commands.
Solver Numerical Differences
The external solver may give different results depending on the
FPU control settings. One example that affects XPRESS 13.26 under
i386_linux is that the floats are normally in `extended precision' of
80 bits in the hardware registers. This is the default used when
ECLiPSe is started. However, if embedded Java is used, Java sets the
float precision to double, i.e. 64 bits. With XPRESS 13.26, this can
result in different behaviours of the solver, e.g. Retimer can give
different alternative answers.
Debugging the External Solver
When an error occurs in using eplex (program crash, unexpected abort,
or incorrect result returned), the error can be due to ECLiPSe
(either C or Prolog level), or the external solver. In the case of the
external solver, we cannot fix the bug, but we can report it so that it
can be fixed (and perhaps workaround the problem in our own code).
In order to report a problem to Dash or ILOG, we need to show that the
problem
is due to their external solver. Several methods are available to do
this:
- if the problem occur while the external solver is solving a
problem, the problem state can be saved before it is solved by the
external solver. If the problem also occurs when the saved problem is
loaded back into (an interactive) session of the solver, then the saved
problem can be used in the bug report. Eplex supports the saving of a
problem before solving it with the option write_before_solve when the
problem is setup. The problem is written using as high a
precision as possible when written as lp or mps format. However,
sometimes the problem may not occur with these formats, but would occur
when written using the binary format. The disadvantage of the binary
format is that it is solver and platform specific, and is non
human-readable.
- log the external solver calls by eplex. See next section for more
details.
Other useful methods to aid in debugging is to run the whole ECLiPSe
session under a memory debugger like valgrind. This can show up
problems both in ECLiPSe and the external solver.
Logging External Solver Calls
There are extra code at the C level which are used to generate a log of
the calls made to the external solver. These code are only compiled if
the macro LOG_CALLS is defined. The log consists of the calls, plus
extra support code, so that only a header file and the toplevel
procedure need to be added to generate a pure C program, which can then
be used to make bug reports to Dash or ILOG.
Structure of the log code
The log code consists of a number of step_N procedures, where N is an
integer. Each such procedure makes a call to the appropriate optimise
function of the external procedure, after setting up the data and
making the other supporting calls to the solver. After the optimise
call, a new step_N procedure, with N incremented by 1.
The data needed for the calls is stored in the same C level problem
descriptor lp_desc that the eplex code used. This is defined in an
include file, along with some procedures and variables that the log
code use. There are three include files, for the three versions of log
code generated:
Xpress 13+ |
bugxprs.h |
Xpress 12 |
bugxprs12.h |
CPlex (6.5+) |
bugcpx.h |
The include file has the following support variables:
struct lp_desc *lpd;
struct lp_desc *lpdmat[20]; /* change size if more lpd used */
double objval;
int res,err;
lpdmat[] is an array to support multiple handles. The generated log
code loads the appropriate lp_desc from lpdmat into lpd at the start of
a step_N procedure, and at any time the handle changes. Note that the
size of lpdmat may need to be adjusted upwards if the logged program
uses more handles than is defined. objval is used to store the
objective value if required (getting the objective value is not
automatically generated in the log code), and res and err are used to
store any return codes from function calls.
Finally, a main procedure needs to be added to call all the step_N
procedures in sequence, e.g. if there are three steps, and the
solver is Xpress 13+:
main() {
XPRSinit(NULL);
XPRScreateprob(&cpx_env);
step_0();
step_1();
step_2();
}
In this case, the cpx_env is the dummy problem where all the default
parameter values are stored.
Logging macros
Several macros are defined to minimise the amount of special logging
code needed:
- Call(Ret, Proc):
Generates a call to Proc in the form Ret = Proc, and a log for Proc if
LOG_CALL is defined.
- CallN(Proc): Generates a
call to Proc in the form Proc, and a log for Proc if LOG_CALL is
defined. This should be used if Proc should be called without getting
the return code.
- Log1(Proc, A1):
Generates a log for Proc if LOG_CALL is defined. No actual call is
generated, so seperate code for the call is needed. This macro should
be used when an argument for Proc needs to be supplied dynamically at
run-time. Proc can be written in printf style with a leading % for the
argument supplied in A1.
- Log2(Proc, A1, A2): Same
as Log1, except it allows two dynamic arguments.
- Log3(Proc, A1, A2, A3):
Same as Log1, except it allows three dynamic arguments.
- Log4(Proc, A1, A2, A3, A4):
Same as Log1, except it allows four dynamic arguments.
- Log5(Proc,A1,A2,A3,A4,A5): Same as Log1, except it allows
five dynamic arguments.
In addition, logging code is also given between #ifdef LOG_CALLS, e.g.
for loading the various data structures.
The logged call is generated by printfs, to the log_output stream.
In order to allow macro transformation for the logged code to be
performed, the call is wrapped in a Transform_Quoted macro, which
performs the transformation even though the logged call occurs inside
the printf quotes.