Controlling the solvers from within METHODS
From ASCEND
We propose to add support for two new statements in the ASCEND language, via a new module called "slvreq".
USE would be used to request that a certain solver be assigned to the problem in hand:
SET would be used to request to set certain solver parameters on the current simulation.
Example:
MODEL mymodel; x,y IS_A solver_var; y = x^2 - x*4 + 4; METHODS; METHOD on_load; FIX y; y := 0; USE QRSlv; SET QRSlv.convopt := 'RELNOM_SCALE'; END on_load; END mymodel;
Because the current ASCEND API is defined in such a way that solvers are only loosely associated with simulations via the user interface layer, these statements will require that the request is routed through the user interface layer.
There needs to be a way for the user interface to register its ability to receive requests to set the solver and to set solver parameters, by passing pointers to hook functions down into libascend. There also needs to be a place where 'user_data' from the interface can be stored, for example in the case where a user interface permits multiple simulations to be loaded at once.
Question: Where should libascend store such pointers? A natural place seems like the SimulationInstance, providing we can get reasonably efficient access to it from further down the tree.
Libascend would store these hook pointers in the simulation instance. It seems that we can always get back to the Simulation instance via the function FindSimulationAncestors in ascend/compiler/instquery.h.
When a method is being run, ASCEND knows the instance in question and can use FindSimulationAncestors to locate the simulation instance. It can then obtain the slvreq hook function pointers. The appropriate function can then be called, passing the USE or SET request back to the user interface.
Question: is it really necessary to repeat the solver name in the SET statement?
Question: in some modelling environment, it is possible to define 'subsolvers'. For example, a conditional solver might be able to make use of either CONOPT or IPOPT for optimisation, so it might be necessary to provide syntax that would work for this case.
Question: is it OK to associate the hook functions with the simulation instance? Is there any case where this data should be stored somewhere else?
See also Solver NOTES, another proposed solution to this problem.
Expected call sequence
The API is looking something like this: slvreq:ascend/compiler/slvreq.h
Process flow:
- GUI is started
- GUI loads a model
- GUI instantiates model
- GUI runs slvreq_register_hooks
- libascend stores register hooks in SimulationInstance
- GUI requests 'on_load' method to be run.
- libascend starts executing method statements
- libascend encounters 'USE' statement.
- libascend locates SimulationInstance and find slvreq_hooks object.
- libascend runs slvreq_hooks->set_solver_fn.
- GUI received request to set solver
- GUI checks that solver is elegible for this problem
- GUI sets solver as requests (or writes an error message and returns error code)
- libascend proceeds with more method statements
- libascend find 'SET' statment
- libascend runs slvreq_hooks->set_param_fn (passes string value of requested solver parameter to GUI)
- GUI receives reqest
- GUI either
- finds that solvername.parametername is valid, and sets the parameter (after casting it to the appropriate type)
- finds that solvername.parametername is not valid, and returns an error code
- libascend proceeds with more METHOD statments...
Python implementation
For this approach to work with the Python GUI, it would require adding some methods in the Simulation class (C++ code) and some more function wrappers in the SWIG interface, such that something like the following would be possible:
# global variable containing our 'current' solver: mysolver = None def setsolver(name,sim): try: mysolver = ascpy.Solver(name) # (Note: canSolve is not implemented yet...) if not mysolver.canSolve(sim): print "solver %s is not eligible!" % name raise Exception("invalid solver") except: mysolver = None return 1; return 0 self.L.load('mymodelfile.a4c') T = self.L.findType('mymodel') M = T.getSimulation('sim',False) # register the setsolver function so that 'USE' statements are able to operate: M.setSolverRequestHooks(setsolver,None) # run the on_load method (we expect a 'USE' statement callback) M.run(T.getMethod('on_load')) if mysolver: # only if a solver has now been assigned... M.solve(mysolver, ascpy.SolverReporter()) else print "no solver selected"
Note that in the case that the setSolverRequesthooks function was not called, METHOD execution could either fail (and throw an exception back to Python) or we could make it so that a warning message was all that was output. In either case, a model that does not make use of the "USE" statement would work exactly as normal.

