Implementing an LP/MIP solver interface
Topics covered in this chapter:
- Example
- Structures for passing information
- Implementation of subroutines
- Implementing a solver callback
- Generating names for matrix entries
The Mosel NI publishes a special set of functionality that provides access to the matrix-based representation of optimization problems formulated using the Mosel types mpvar, linctr, mpproblem, that is, LP and MIP problems. These NI functions can be used for implementing interfaces to optimization solvers that are available in the form of a C/C++ library.
Example
This chapter explains how to implement a basic Mosel module myxprs for using Xpress Optimizer as the solver for optimization models stated in Mosel. The use of the new module from Mosel looks as follows for its simplest form that provides starting of the solver, solution retrieval, and access to solver parameters.
model "Problem solving and solution retrieval" uses "myxprs" ! Load the solver module declarations x,y: mpvar ! Some decision variables pb: mpproblem ! (Sub)problem end-declarations procedure printsol if getprobstat = MYXP_OPT then writeln("Solution: ", getobjval, ";", x.sol, ";", y.sol) else writeln("No solution found") end-if end-procedure Ctr1:= 3*x + 2*y <= 400 Ctr2:= x + 3*y <= 310 MyObj:= 5*x + 20*y ! Setting solver parameters setparam("myxp_verbose", true) ! Display solver log setparam("myxp_timelimit", 10) ! Set a time limit ! Solve the problem (includes matrix generation) maximize(MyObj) ! Retrieve a solver parameter writeln("Solver status: ", getparam("myxp_lpstatus")) ! Access solution information printsol ! Turn poblem into a MIP x is_integer; y is_integer ! Solve the modified problem maximize(MyObj) printsol ! **** Define and solve a (sub)problem **** with pb do 3*x + 2*y <= 350 x + 3*y <= 250 maximize(5*x + 20*y) printsol end-do end-model
Sections Implementation of callback handling and Generating names for matrix entries show how to extend this initial version with a solution callback and a matrix export subroutine including names generation.
Structures for passing information
A minimal implementation of a solver module needs to do the following:
- Modeling functionality:
- define subroutines to start an optimization run and retrieve solution values
- provide access to solver parameters
- if supported by the solver, provide support for handling multiple problems
- NI functionality:
- implement a reset and an unload service
- initialize the module and the required interface structures
To start with, let us take a look at the structures that are required for exchanging information between Mosel and the external program.
List of subroutines
The minimal set of entries for the list of subroutines would be just the calls to minimization/maximization. Our implementation adds a function to retrieve the problem status information, alternative spelling for the optimization routines, and it also provides the access routines for module control parameters that are required for the implementation of solver parameters.
static XPRMdsofct tabfct[]= { {"",XPRM_FCT_GETPAR,XPRM_TYP_NOT,0,NULL,slvlc_getpar}, {"",XPRM_FCT_SETPAR,XPRM_TYP_NOT,0,NULL,slvlc_setpar}, {"getprobstat",2000,XPRM_TYP_INT,0,NULL,slvlc_getpstat}, {"minimise",2100,XPRM_TYP_NOT,1,"c",slvlc_minim}, {"minimize",2100,XPRM_TYP_NOT,1,"c",slvlc_minim}, {"maximise",2101,XPRM_TYP_NOT,1,"c",slvlc_maxim}, {"maximize",2101,XPRM_TYP_NOT,1,"c",slvlc_maxim} };
List of parameters
In terms of an example, we provide access to a few controls of Xpress Optimizer, and the module also shows how to implement a verbosity flag, resulting in the following list of module parameters:
static struct /* Parameters published by this module */ { char *name; int type; } myxprsparams[]= { {"myxp_verbose",XPRM_TYP_BOOL|XPRM_CPAR_READ|XPRM_CPAR_WRITE}, {"myxp_timelimit",XPRM_TYP_REAL|XPRM_CPAR_READ|XPRM_CPAR_WRITE}, {"myxp_lpstatus",XPRM_TYP_INT|XPRM_CPAR_READ}, {"myxp_lpobjval",XPRM_TYP_REAL|XPRM_CPAR_READ}, };
The problem and LP status parameters return values that are best implemented via module constants, such as:
static XPRMdsoconst tabconst[]= { XPRM_CST_INT("MYXP_INF",XPRM_PBINF), /* Mosel status codes */ XPRM_CST_INT("MYXP_OPT",XPRM_PBOPT), XPRM_CST_INT("MYXP_OTH",XPRM_PBOTH), XPRM_CST_INT("MYXP_UNF",XPRM_PBUNF), XPRM_CST_INT("MYXP_UNB",XPRM_PBUNB), XPRM_CST_INT("MYXP_LP_OPTIMAL",XPRS_LP_OPTIMAL), /* Solver status codes */ XPRM_CST_INT("MYXP_LP_INFEAS",XPRS_LP_INFEAS), XPRM_CST_INT("MYXP_LP_CUTOFF",XPRS_LP_CUTOFF) };
List of types
The list of types has a single entry: a solver module needs to extend the Mosel type mpproblem with its own implementation.
static XPRMdsotyp tabtyp[]= { {"mpproblem.mxp",1,XPRM_DTYP_PROB|XPRM_DTYP_APPND,slv_pb_create, slv_pb_delete,NULL,NULL,slv_pb_copy} };
The following structure implements the problem type for our module, Mosel will maintain one instance of this type for each mpproblem object.
typedef struct SlvPb { struct SlvCtx *slctx; /* Solver context */ XPRSprob xpb; int have; int is_mip; double *solval; /* Structures for storing solution values */ double *dualval; double *rcostval; double *slackval; XPRMcontext saved_ctx; /* Mosel context (used by callbacks) */ struct SlvPb *prev,*next; } s_slvpb;
A solver context definition is shown below in Section Module context.
List of services
The services PARAM and PARLST are required for the handling of module parameters, RESET and UNLOAD manage the access to the solver library.
static XPRMdsoserv tabserv[]= { {XPRM_SRV_PARAM, (void *)slv_findparam}, {XPRM_SRV_PARLST, (void *)slv_nextparam}, {XPRM_SRV_RESET, (void *)slv_reset}, {XPRM_SRV_UNLOAD, (void *)slv_quitlib} };
Module context
The module context holds the type ID for the extended mpproblem type, module options, and a list of references to the problems that have been created by this module:
typedef struct SlvCtx /* A context for this module */ { int pbid; /* ID of type "mpproblem.mxp" */ int options; /* Runtime options */ s_slvpb *probs; /* List of created problems */ } s_slvctx;
A specific interface structure required by the matrix generation is the following MIP solver interface definition that defines the shorthands to be used for identifying constraint and variable types and specifies the names of the functions for matrix generation, cleaning up solution information, and retrieving solution values for decision variables and constraints:
static mm_mipsolver xpress= {{'N','G','L','E','R','1','2'}, {'+','I','B','P','S','R'}, slv_loadmat, slv_clearsol, slv_getsol_v, slv_getsol_c};
Interface structure
The interface structure holds as usual the definition of the four tables (constants, subroutines, types, and services).
static XPRMdsointer dsointer= { sizeof(tabconst)/sizeof(XPRMdsoconst), tabconst, sizeof(tabfct)/sizeof(XPRMdsofct), tabfct, sizeof(tabtyp)/sizeof(XPRMdsotyp), tabtyp, sizeof(tabserv)/sizeof(XPRMdsoserv), tabserv };
Initialization function
The module initialization function performs the initialization of the solver library.
DSO_INIT myxprs_init(XPRMnifct nifct, int *interver,int *libver, XPRMdsointer **interf) { int r; *interver=XPRM_NIVERS; /* The interface version we are using */ *libver=XPRM_MKVER(0,0,1); /* The version of the module: 0.0.1 */ *interf=&dsointer; /* Our module interface structure */ r=XPRSinit(NULL); /* Initialize the solver */ if((r!=0)&&(r!=32)) { nifct->dispmsg(NULL,"myxprs: I cannot initialize Xpress Optimizer.\n"); return 1; } mm=nifct; /* Retrieve the Mosel NI function table */ return 0; }
Implementation of subroutines
Solver library calls
The first two entries of the list of subroutines concern the handling of module parameters, with the exception of myxp_verbose that is a setting for the module itself, all other parameters are straightforward mappings of solver control parameters published by the solver library.
/**** Getting a control parameter ****/ static int slv_lc_getpar(XPRMcontext ctx,void *libctx) { s_slvctx *slctx; int n; double r; slctx=libctx; n=XPRM_POP_INT(ctx); switch(n) { case 0: XPRM_PUSH_INT(ctx,(slctx->options&OPT_VERBOSE)?1:0); break; case 1: XPRSgetdblcontrol(SLVCTX2PB(slctx)->xpb,XPRS_TIMELIMIT,&r); XPRM_PUSH_REAL(ctx,r); break; case 2: XPRSgetintattrib(SLVCTX2PB(slctx)->xpb,XPRS_LPSTATUS,&n); XPRM_PUSH_INT(ctx,n); break; case 3: XPRSgetdblattrib(SLVCTX2PB(slctx)->xpb,XPRS_LPOBJVAL,&r); XPRM_PUSH_REAL(ctx,r); break; default: mm->dispmsg(ctx,"myxprs: Wrong control parameter number.\n"); return XPRM_RT_ERROR; } return XPRM_RT_OK; } /**** Setting a control parameter ****/ static int slv_lc_setpar(XPRMcontext ctx,void *libctx) { s_slvctx *slctx; int n; slctx=libctx; n=XPRM_POP_INT(ctx); switch(n) { case 0: slctx->options=XPRM_POP_INT(ctx)?(slctx->options|OPT_VERBOSE):(slctx->options&~OPT_VERBOSE); break; case 1: XPRSsetdblcontrol(SLVCTX2PB(slctx)->xpb,XPRS_TIMELIMIT,XPRM_POP_REAL(ctx)); break; default: mm->dispmsg(ctx,"myxprs: Wrong control parameter number.\n"); return XPRM_RT_ERROR; } return XPRM_RT_OK; }
The subroutine getprobstat exposes the Mosel problem status at the model level (this status value is populated after every solver run, see implementation of function slv_optim below).
static int slv_lc_getpstat(XPRMcontext ctx,void *libctx) { XPRM_PUSH_INT(ctx,mm->getprobstat(ctx)&XPRM_PBRES); return XPRM_RT_OK; }
The two module functions implementing the minimize and maximize subroutines map to the same function slv_optim.
static int slv_lc_maxim(XPRMcontext ctx,void *libctx) { XPRMlinctr obj; obj=XPRM_POP_REF(ctx); return slv_optim(ctx,(s_slvctx *)libctx,OBJ_MAXIMIZE,obj); } static int slv_lc_minim(XPRMcontext ctx,void *libctx) { XPRMlinctr obj; obj=XPRM_POP_REF(ctx); return slv_optim(ctx,(s_slvctx *)libctx,OBJ_MINIMIZE,obj); }
The function slv_optim first clears any existing solution information, it then generates and loads the matrix representation of the problem into the solver and starts the actual solving process. After termination of the solver run it retrieves problem status information in order to populate Mosel's problem status flag.
static int slv_optim(XPRMcontext ctx, s_slvctx *slctx, int objsense, XPRMlinctr obj) { int c,i; s_slvpb *slpb; int result; double objval; slpb=SLVCTX2PB(slctx); slpb->saved_ctx=ctx; /* Save current context for callbacks */ slv_clearsol(ctx,slpb); /* Call NI function 'loadmat' to generate and load the matrix */ if(mm->loadmat(ctx,obj,NULL,MM_MAT_FORCE,&xpress,slpb)!=0) { mm->dispmsg(ctx,"myxprs: loadprob failed.\n"); slpb->saved_ctx=NULL; return XPRM_RT_ERROR; } /* Set optimization direction */ XPRSchgobjsense(slpb->xpb, (objsense==OBJ_MINIMIZE)?XPRS_OBJ_MINIMIZE:XPRS_OBJ_MAXIMIZE); mm->setprobstat(ctx,XPRM_PBSOL,0); /* Solution available for callbacks */ if(!slpb->is_mip) { /* Solve an LP problem */ c=XPRSlpoptimize(slpb->xpb,""); if(c!=0) { mm->dispmsg(ctx,"myxprs: optimisation failed.\n"); slpb->saved_ctx=NULL; return XPRM_RT_ERROR; } /* Retrieve solution status */ XPRSgetintattrib(slpb->xpb,XPRS_PRESOLVESTATE,&i); if(i&128) { XPRSgetdblattrib(slpb->xpb,XPRS_LPOBJVAL,&objval); result=XPRM_PBSOL; } else { objval=0; result=0; } XPRSgetintattrib(slpb->xpb,XPRS_LPSTATUS,&i); switch(i) { case XPRS_LP_OPTIMAL: result|=XPRM_PBOPT; break; case XPRS_LP_INFEAS: result|=XPRM_PBINF; break; case XPRS_LP_CUTOFF: result|=XPRM_PBOTH; break; case XPRS_LP_UNFINISHED: result|=XPRM_PBUNF; break; case XPRS_LP_UNBOUNDED: result|=XPRM_PBUNB; break; case XPRS_LP_CUTOFF_IN_DUAL: result|=XPRM_PBOTH; break; case XPRS_LP_UNSOLVED: result|=XPRM_PBOTH; break; } } else { /* Solve an MIP problem */ c=XPRSmipoptimize(slpb->xpb,""); if(c!=0) { mm->dispmsg(ctx,"myxprs: optimization failed.\n"); slpb->saved_ctx=NULL; return XPRM_RT_ERROR; } /* Retrieve solution status */ XPRSgetintattrib(slpb->xpb,XPRS_MIPSTATUS,&i); switch(i) { case XPRS_MIP_LP_NOT_OPTIMAL: objval=0; result=XPRM_PBUNF; break; case XPRS_MIP_LP_OPTIMAL: objval=0; result=XPRM_PBUNF; break; case XPRS_MIP_NO_SOL_FOUND: /* Search incomplete: no solution */ objval=0; result=XPRM_PBUNF; break; case XPRS_MIP_SOLUTION: /* Search incomplete: there is a solution */ XPRSgetdblattrib(slpb->xpb,XPRS_MIPOBJVAL,&objval); result=XPRM_PBUNF|XPRM_PBSOL; slpb->have|=HAVEMIPSOL; break; case XPRS_MIP_INFEAS: /* Search complete: no solution */ objval=0; result=XPRM_PBINF; break; case XPRS_MIP_OPTIMAL: /* Search complete: best solution available */ XPRSgetdblattrib(slpb->xpb,XPRS_MIPOBJVAL,&objval); result=XPRM_PBSOL|XPRM_PBOPT; slpb->have|=HAVEMIPSOL; break; case XPRS_MIP_UNBOUNDED: objval=0; result=XPRM_PBUNB; break; } if(!(result&XPRM_PBSOL)) { /* If no MIP solution try to get an LP solution */ XPRSgetintattrib(slpb->xpb,XPRS_PRESOLVESTATE,&i); if(i&128) { XPRSgetdblattrib(slpb->xpb,XPRS_LPOBJVAL,&objval); result|=XPRM_PBSOL; } } } /* Record solution status and objective value */ mm->setprobstat(ctx,result,objval); slpb->saved_ctx=NULL; return 0; }
Implementation of MIP solver interface functions
The following functions implement the MIP solver interface functions ('loadmat', 'clearsol', 'getsol_v', 'getsol_c') that are communicated to the NI routine loadmat via the MIP solution interface structure xpress (for its definition see Section Module context).
The 'loadmat' function loads a matrix that is held in Mosel structures into the LP or MIP solver.
static int slv_loadmat(XPRMcontext ctx, void *mipctx, mm_matrix *m) { s_slvpb *slpb; s_slvctx *slctx; char pbname[80]; int c,r; slpb=mipctx; slctx=slpb->slctx; slv_clearsol(ctx,slpb); slpb->is_mip=(m->ngents>0) || (m->nsos>0); sprintf(pbname,"xpb%p",slpb); if(slpb->is_mip) r=XPRSloadmip(slpb->xpb,pbname,m->ncol,m->nrow, m->qrtype,m->rhs,m->range,m->obj, m->mstart,NULL,m->mrwind,m->dmatval,m->dlb,m->dub, m->ngents,m->nsos,m->qgtype,m->mgcols,m->mplim,m->qstype, m->msstart,m->mscols,m->dref); else r=XPRSloadlp(slpb->xpb,pbname,m->ncol,m->nrow, m->qrtype,m->rhs,m->range,m->obj, m->mstart,NULL,m->mrwind,m->dmatval,m->dlb,m->dub); /* Objective constant term */ if(!r) { c=-1;r=XPRSchgobj(slpb->xpb,1, &c, &(m->fixobj)); } return r; }
The 'clearsol' function frees up solution information held in the solver problem interface structures.
static void slv_clearsol(XPRMcontext ctx, void *mipctx) { s_slvpb *slpb; slpb=mipctx; Free(&(slpb->solval)); Free(&(slpb->dualval)); Free(&(slpb->rcostval)); Free(&(slpb->slackval)); slpb->have=0; } /**** Free + reset memory ****/ static void Free(void *ad) { free(*(void **)ad); *(void **)ad=NULL; }
The 'getsol_v' and 'getsol_c' routines serve to retrieve the solution values for decision variables and constraints from the solver. These implementations retrieve the entire arrays at once and any subsequent calls return the information saved in the solver interface structures.
/**** Solution information for decision variables ****/ static double slv_getsol_v(XPRMcontext ctx, void *mipctx, int what, int col) { s_slvpb *slpb; int ncol; slpb=mipctx; XPRSgetintattrib(slpb->xpb,XPRS_INPUTCOLS,&ncol); if(what) { if(!(slpb->have&HAVERCS)) { if(slpb->rcostval==NULL) { if((slpb->rcostval=malloc(ncol*sizeof(double)))==NULL) { mm->dispmsg(ctx,"myxprs: Out of memory error.\n"); return 0; } } if(slpb->have&HAVEMIPSOL) /* No rcost for a MIP => 0 */ memset(slpb->rcostval,0,ncol*sizeof(double)); else XPRSgetredcosts(slpb->xpb,NULL,slpb->rcostval,0,ncol-1); slpb->have|=HAVERCS; } return slpb->rcostval[col]; } else { if(!(slpb->have&HAVESOL)) { if(slpb->solval==NULL) { if((slpb->solval=malloc(ncol*sizeof(double)))==NULL) { mm->dispmsg(ctx,"myxprs: Out of memory error.\n"); return 0; } } XPRSgetsolution(slpb->xpb,NULL,slpb->solval,0,ncol-1); slpb->have|=HAVESOL; } return slpb->solval[col]; } }
/**** Solution information for linear constraints ****/ static double slv_getsol_c(XPRMcontext ctx, void *mipctx, int what, int row) { s_slvpb *slpb; int nrow; slpb=mipctx; XPRSgetintattrib(slpb->xpb,XPRS_INPUTROWS,&nrow); if(what) { if(!(slpb->have&HAVEDUA)) { if(slpb->dualval==NULL) { if((slpb->dualval=malloc(nrow*sizeof(double)))==NULL) { mm->dispmsg(ctx,"myxprs: Out of memory error.\n"); return 0; } } if(slpb->have&HAVEMIPSOL) /* No dual for a MIP => 0 */ memset(slpb->dualval,0,nrow*sizeof(double)); else XPRSgetduals(slpb->xpb,NULL,slpb->dualval,0,nrow-1); slpb->have|=HAVEDUA; } return slpb->dualval[row]; } else { if(!(slpb->have&HAVESLK)) { if(slpb->slackval==NULL) { if((slpb->slackval=malloc(nrow*sizeof(double)))==NULL) { mm->dispmsg(ctx,"myxprs: Out of memory error.\n"); return 0; } } XPRSgetslacks(slpb->xpb,NULL,slpb->slackval,0,nrow-1); slpb->have|=HAVESLK; } return slpb->slackval[row]; } }
Implementation of services
The RESET service function creates a new module context if none is provided in the argument, on a subsequent call where the module context argument is populated this context will be released after freeing all data structures that may have been created via the solver library.
/**** Reset the myxprs interface for a run ****/ static void *slv_reset(XPRMcontext ctx, void *libctx) { s_slvctx *slctx; /* End of execution: release context */ if(libctx!=NULL) { slctx=libctx; /* Release all remaining problems */ while(slctx->probs!=NULL) { slv_pb_delete(ctx,slctx,slctx->probs,-1); } free(slctx); return NULL; } else { /* Begin of execution: create context */ if((slctx=malloc(sizeof(s_slvctx)))==NULL) { mm->dispmsg(ctx,"myxprs: Out of memory error.\n"); return NULL; } memset(slctx,0,sizeof(s_slvctx)); /* Record the problem ID of our problem type */ mm->gettypeprop(ctx,mm->findtypecode(ctx,"mpproblem.mxp"),XPRM_TPROP_PBID, (XPRMalltypes*)&(slctx->pbid)); return (void *)slctx; } }
The UNLOAD service terminates the solver library (frees up the licence) that has been initialized from the module initialization.
/**** Called when unloading the library ****/ static void slv_quitlib(void) { if(mm!=NULL) { XPRSfree(); mm=NULL; } }
The implementation of the module parameter access services PARAM and PARLST is similar to what we have seen for other modules (e.g. see Section Services related to parameters).
/**** Find a control parameter ****/ static int slv_findparam(const char *name,int *type,int why,XPRMcontext ctx, void *libctx) { int n; for(n=0;n<SLV_NBPARAM;n++) { if(strcmp(name,myxprsparams[n].name)==0) { *type=myxprsparams[n].type; return n; } } return -1; } /**** Return the next parameter for enumeration ****/ static void *slv_nextparam(void *ref,const char **name,const char **desc, int *type) { size_t cst; cst=(size_t)ref; if((cst<0)||(cst>=SLV_NBPARAM)) return NULL; else { *name=myxprsparams[cst].name; *type=myxprsparams[cst].type; *desc=NULL; return (void *)(cst+1); } }
Handling optimization problems
Each Mosel model creates a default optimization problem of type mpproblem holding the constraints that are defined in the model. Further (sub)problems can be defined explicitly by the model developer, such as in the example shown at the beginning of this chapter (Section Example).
A solver module needs to implement an extension to the mpproblem type. Typically, the underlying data structure will include a reference to the solver problem representation, some status flags and structures to store solution information (see definition in Section List of types above). For our Xpress Optimizer example we have implemented the type handling routines to create, delete, and copy optimization problems. If the underlying solver can only handle a single problem, the implementation of the 'create' routine should prevent the creation of more than one problem and a 'copy' routine is most likely not required.
The following implementation of a problem creation routine for Xpress Optimizer creates the Optimizer problem, redirects the Optimizer output onto Mosel and it also defines some logging callbacks in order to intercept a program interruption.
/**** Create a new "problem" ****/ static void *slv_pb_create(XPRMcontext ctx, void *libctx, void *toref, int type) { s_slvctx *slctx; s_slvpb *slpb; int i; slctx=libctx; if((slpb=malloc(sizeof(s_slvpb)))==NULL) { mm->dispmsg(ctx,"myxprs: Out of memory error.\n"); return NULL; } memset(slpb,0,sizeof(s_slvpb)); i=XPRScreateprob(&(slpb->xpb)); if((i!=0)&&(i!=32)) { mm->dispmsg(ctx,"myxprs: I cannot create the problem.\n"); free(slpb); return NULL; } slpb->slctx=slctx; /* Redirect solver messages to the Mosel streams */ XPRSaddcbmessage(slpb->xpb,slvcb_output,slpb,0); XPRSsetintcontrol(slpb->xpb,XPRS_OUTPUTLOG,1); /* Define log callbacks to report program interruption */ XPRSaddcblplog(slpb->xpb,(void*)slvcb_stopxprs,slpb,0); XPRSaddcbcutlog(slpb->xpb,(void*)slvcb_stopxprs,slpb,0); XPRSaddcbmiplog(slpb->xpb,(void*)slvcb_stopxprs,slpb,0); XPRSaddcbbarlog(slpb->xpb,(void*)slvcb_stopxprs,slpb,0); if(slctx->probs!=NULL) { slpb->next=slctx->probs; slctx->probs->prev=slpb; } /* else we are creating the main (default) problem */ slctx->probs=slpb; return slpb; }
The problem deletion routine needs to update the list of problems saved in the solver problem interface structure.
/**** Delete a "problem" ****/ static void slv_pb_delete(XPRMcontext ctx,void *libctx,void *todel,int type) { s_slvctx *slctx; s_slvpb *slpb; slctx=libctx; slpb=todel; slv_clearsol(ctx,slpb); XPRSdestroyprob(slpb->xpb); if(slpb->next!=NULL) /* Last in list */ slpb->next->prev=slpb->prev; if(slpb->prev==NULL) /* First in list */ slctx->probs=slpb->next; else slpb->prev->next=slpb->next; free(slpb); }
A problem is copied without duplicating the solution information.
/**** Copy/reset/append problems: simply clear data of the destination ****/ static int slv_pb_copy(XPRMcontext ctx,void *libctx,void *toinit,void *src,int ust) { s_slvpb *slpb; slpb=toinit; if(XPRM_CPY(ust)<XPRM_CPY_APPEND) slv_clearsol(ctx,slpb); return 0; }
Implementing a solver callback
Example
Many programs, and in particular LP/MIP solvers, provide the possibility to interact with the program during its execution by means of callbacks. In terms of an example, we will show here how to implement an 'INTSOL' callback for Xpress Optimizer, that is, an entry point for calling a Mosel subroutine every time the solver has found a new MIP solution. The corresponding Mosel code might look as follows (notice that the Mosel subroutine is flagged as public in order to make it visible for external programs):
public procedure intsol writeln("!!! New solution !!!") writeln("Solution: ", getobjval, "; ", x.sol, "; ", y.sol), "; obj=", getparam("myxp_lpobjval")) end-procedure ! Define the procedure 'intsol' as the solver INTSOL callback routine setcbintsol("intsol")
An alternative implementation of this callback directly works with the reference to the subroutine instead of a string with its name, in which case the public marker for the definition of the procedure is not required:
procedure intsol writeln("!!! New solution !!!") writeln("Solution: ", getobjval, "; ", x.sol, "; ", y.sol), "; obj=", getparam("myxp_lpobjval")) end-procedure ! Define the procedure 'intsol' as the solver INTSOL callback routine setcbintsol(->intsol)
Implementation of callback handling
The handling of callbacks by the Mosel NI is not specific to the matrix / MIP solver interface, it can be applied for any external program that provides entry points for callbacks. In our case, the subroutine setcbintsol is declared via the following entry in the list of subroutines for the form taking a string argument.
{"setcbintsol",2102,XPRM_TYP_NOT,1,"s",slv_lc_setcbintsol}
The implementation of the 'setcbintsol' routine in the module function slvlc_setcbintsol checks whether the specified subroutine has the expected format before saving its reference in the problem structure. It also needs to handle (increase/decrease) the reference count for the subroutine reference that is passed in argument.
static int slv_lc_setcbintsol(XPRMcontext ctx,void *libctx) { s_slvctx *slctx; s_slvpb *slpb; XPRMalltypes result; const char *procname,*partyp; int nbpar,type; slctx=libctx; slpb=SLVCTX2PB(slctx); procname=XPRM_POP_REF(ctx); if(slpb->cb_intsol!=NULL) { /* Decrease reference count when deleting or redefining */ mm->delref(ctx,XPRM_STR_PROC,slpb->cb_intsol); slpb->cb_intsol=NULL; } if(procname!=NULL) { /* The specified entity must be a procedure */ if(XPRM_STR(mm->findident(ctx,procname,&result,XPRM_FID_NOLOC))!= XPRM_STR_PROC) { mm->dispmsg(ctx,"myxprs: Wrong subroutine type for callback `intsol'.\n"); return XPRM_RT_ERROR; } do { /* The specified procedure must not have any arguments */ mm->getprocinfo(result.proc,&partyp,&nbpar,&type); type=XPRM_TYP(type); if((type==XPRM_TYP_NOT)&&(nbpar==0)) break; result.proc=mm->getnextproc(result.proc); } while(result.proc!=NULL); if(result.proc==NULL) { mm->dispmsg(ctx,"myxprs: Wrong procedure type for callback `intsol'.\n"); return XPRM_RT_ERROR; } else /* Augment reference count */ slpb->cb_intsol=mm->newref(ctx,XPRM_STR_PROC,result.proc); } return XPRM_RT_OK; }
The version of the subroutine setcbintsol that takes a reference to the subroutine as its argument is declared via the following entry in the list of subroutines.
{"setcbintsol", 2103, XPRM_TYP_NOT,1,"F()", slv_lc_setcbintsol_pr}
The implementation of this second form via the module function slv_lc_setcbintsol_pr is considerably simplified compared with the previous version, leaving out the type checks that are in this case performed directly by the Mosel compiler — we just need to save the reference to the subroutine in the problem structure and suitably increase/decrease the reference count for this subroutine reference.
static int slv_lc_setcbintsol_pr(XPRMcontext ctx,void *libctx) { s_slvctx *slctx; s_slvpb *slpb; mm_proc proc; slctx=libctx; slpb=SLVCTX2PB(slctx); proc=XPRM_POP_REF(ctx); if(proc==NULL) { mm->dispmsg(ctx,"myxprs: NULL reference.\n"); return XPRM_RT_ERROR; } else { /* Handling reference count on subroutine reference */ if(slpb->cb_intsol!=NULL) mm->delref(ctx,XPRM_STR_PROC,slpb->cb_intsol); slpb->cb_intsol=mm->newref(ctx,XPRM_STR_PROC,proc); } return XPRM_RT_OK; }
The problem structure s_slvpb has received a new field to store the callback reference:
typedef struct SlvPb { ... XPRMproc cb_intsol; } s_slvpb;
We also add a line for initializing this callback to the problem creation routine slv_pb_create after the creation of the actual problem:
/* Define intsol callback */ XPRSaddcbintsol(slpb->xpb,(void*)slv_cb_intsol,slpb,0);
The callback function slv_cb_intsol needs to have the prototype required by the solver library. In addition to the invocation of the Mosel procedure specified in the model via setcbintsol we also add the handling of user interrupts to this implementation.
static void XPRS_CC slv_cb_intsol(XPRSprob opt_prob,s_slvpb *slpb) { XPRMalltypes result; XPRSprob xpb_save; if(slpb->cb_intsol!=NULL) { xpb_save=slpb->xpb; slpb->xpb=opt_prob; slpb->have=0; if(mm->callproc(slpb->saved_ctx,slpb->cb_intsol,&result)!=0) { mm->stoprun(slpb->saved_ctx); XPRSinterrupt(opt_prob,XPRS_STOP_CTRLC); } slpb->xpb=xpb_save; } }
Generating names for matrix entries
An LP/MIP problem definition held in Mosel can be displayed on screen or exported to a file using the subroutine exportprob. Nevertheless, in particular while developing a solver interface it may be helpful to also have the possibility of writing out the matrix representation directly from the solver.
In our example implementation the matrix gets loaded into the solver through the call to optimization, so writing out the matrix needs to take place after this call:
setparam("myxp_loadnames", true) maximize(MyObj) writeprob("mymat.lp", "l")
When writing out a matrix for debugging purposes one might expect to be able to match the rows and columns to the Mosel modeling entities via their respective names. By default, Mosel does not generate any names for decision variables or constraints in order to maintain a low memory footprint. This feature needs to be added explicitly into the implementation of the loadmat routine that we have seen earlier in this chapter. After a call to the NI function genmpnames the resulting names are collected into the corresponding data structures that are expected by the solver library (uploading names for rows, columns, and SOS separately in the case of Xpress Optimizer).
static int slv_loadmat(XPRMcontext ctx,void *mipctx,mm_matrix *m) { s_slvpb *slpb; s_slvctx *slctx; int c,r; slpb=mipctx; slctx=slpb->slctx; /* Generate names for matrix elements */ if(slctx->options&OPT_LOADNAMES) mm->genmpnames(ctx,MM_KEEPOBJ,NULL,0); /* ... load the problem matrix into the solver ... */ /* Load names if requested */ if(!r && (slctx->options&OPT_LOADNAMES)) { char *names,*n; size_t totlen,totlen2; size_t l; totlen=0; for(c=0;c<m->ncol;c++) { l=strlen(mm->getmpname(ctx,MM_MPNAM_COL,c)); totlen+=l+1; } totlen2=0; for(c=0;c<m->nrow;c++) { l=strlen(mm->getmpname(ctx,MM_MPNAM_ROW,c)); totlen2+=l+1; } if(totlen<totlen2) totlen=totlen2; totlen2=0; for(c=0;c<m->nsos;c++) { l=strlen(mm->getmpname(ctx,MM_MPNAM_SOS,c)); totlen2+=l+1; } if(totlen<totlen2) totlen=totlen2; if((names=malloc(totlen))==NULL) mm->dispmsg(ctx,"myxprs: Not enough memory for loading the names.\n"); else { n=names; for(c=0;c<m->ncol;c++) n+=strlen(strcpy(n,mm->getmpname(ctx,MM_MPNAM_COL,c)))+1; if((r=XPRSaddnames(slpb->xpb,2,names,0,m->ncol-1))!=0) mm->dispmsg(ctx,"myxprs: Error when executing `addnames'.\n"); if(!r && (m->nrow>0)) { n=names; for(c=0;c<m->nrow;c++) n+=strlen(strcpy(n,mm->getmpname(ctx,MM_MPNAM_ROW,c)))+1; if((r=XPRSaddnames(slpb->xpb,1,names,0,m->nrow-1))!=0) mm->dispmsg(ctx,"myxprs: Error when executing `addnames'.\n"); } if(!r && (m->nsos>0)) { n=names; for(c=0;c<m->nsos;c++) n+=strlen(strcpy(n,mm->getmpname(ctx,MM_MPNAM_SOS,c)))+1; if((r=XPRSaddnames(slpb->xpb,3,names,0,m->nsos-1))!=0) mm->dispmsg(ctx,"myxprs: Error when executing `addnames'.\n"); } free(names); } } return r; }
In this subroutine, the loading of names is subject to the presence of the option flag LOADNAMES that is set via a new module parameter myxp_loadnames which is declared via the following entry in the table of parameters:
{"myxp_loadnames",XPRM_TYP_BOOL|XPRM_CPAR_READ|XPRM_CPAR_WRITE}
Implementing the 'writeprob' subroutine
The writeprob routine shown in the Mosel model extract at the beginning of this section needs to be declared in the table of subroutines structure by adding the following line to it:
{"writeprob",2104,XPRM_TYP_NOT,2,"ss",slvlc_writepb}
And the actual implementation in the function slvlc_writepb consists of a call the the solver's matrix output function, along with some error handling such as a check for write access to the specified location:
static int slvlc_writepb(XPRMcontext ctx,void *libctx) { s_slvpb *slpb; int rts; char *dname,*options; char ename[MM_MAXPATHLEN]; slpb=SLCTX2PB((s_slvctx*)libctx); dname=MM_POP_REF(ctx); options=MM_POP_REF(ctx); if((dname!=NULL)&& /* Make sure the file can be created */ (mm->pathcheck(ctx,dname,ename,MM_MAXPATHLEN,MM_RCHK_WRITE|MM_RCHK_IODRV)==0)) { slpb->saved_ctx=ctx; /* Save current context for callbacks */ rts=XPRSwriteprob(slpb->xpb,XNLSconvstrto(XNLS_ENC_FNAME,ename,-1,NULL),options); slpb->saved_ctx=NULL; if(rts) { mm->dispmsg(ctx,"myxprs: Error when executing `writeprob'.\n"); return XPRM_RT_IOERR; } else return XPRM_RT_OK; } else { mm->dispmsg(ctx,"myxprs: Cannot write to '%s'.\n",dname!=NULL?dname:""); return XPRM_RT_IOERR; } }
© 2001-2025 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.