.. py:currentmodule:: or_topas.aos Pyomo Solution Pools -------------------- The *alternative-solutions library* uses solution pools to filter and store solutions generated by an optimizer. The following types of solution pools are currently supported: * ``keep_all`` : This pool stores all solutions. No solutions are filtered out. * ``keep_latest`` : This pool stores the latest ``max_pool_size`` solutions that are added to the pool. * ``max_pool_size`` (non-negative integer) : The maximum number of solutions that are stored. * ``keep_latest_unique`` : This pool stores the latest ``max_pool_size`` solutions, ignoring duplicate solutions. * ``max_pool_size`` (non-negative integer) : The maximum number of solutions that are stored. * ``keep_best`` : This pool stores the best solutions added to the pool. * ``max_pool_size`` (non-negative integer) : The maximum number of solutions that are stored. * ``objective`` (function) : A user-specified function that computes the objective value used for comparisons. * ``abs_tolerance`` (non-negative float) : The absolute tolerance that is used to filter solutions. * ``rel_tolernace`` (non-negative float) : The relative tolerance that is used to filter solutions. * ``sense_is_min`` (bool) : If True, then the pool will keep solutions with the minimal objective values. * ``best_value`` (float) : As solutions are added to this pool, it tracks the best solution value seen for tolerance comparisons. If specified, then this value provides an initial value for the best solution value. A pool manager class is used to manage one-or-more solution pools. This allows for flexible collection of solutions with different criteria. For example, the the best solutions might be stored along with all per-iteration solutions in an optimization solver. The solution generation functions in the *alternative-solutions library* return a :py:class:`PyomoPoolManager`. By default, this pool manager uses a solution pool that keeps the best solutions. However, the user can provide a pool manager that is used to store solutions. For example, we can explicitly create a pool manager that keeps the latest solutions. Consider the previous example, where all feasible solutions are generated: .. doctest:: :skipif: not glpk_available >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk") >>> print( [soln.objective().value for soln in solns] ) [70.0, 70.0, 60.0, 60.0, 50.0, 30.0, 20.0, 10.0, 0.0] Each solution has a unique index: .. doctest:: :skipif: not glpk_available >>> print( [soln.id for soln in solns] ) [0, 1, 2, 3, 4, 5, 6, 7, 8] Now we create a :py:class:`PyomoPoolManager` that is configured with a ``keep_latest`` pool: .. doctest:: :skipif: not glpk_available >>> pool_manager = aos.PyomoPoolManager() >>> context = pool_manager.add_pool(policy=aos.PoolPolicy.keep_latest, max_pool_size=3) >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk", pool_manager=pool_manager) >>> assert id(pool_manager) == id(solns) >>> print( [soln.id for soln in solns] ) [6, 7, 8] The default solution pool has policy ``keep_best`` with name ``None``. If a new Solution pool is added without a name, then the ``None`` pool is replaced. Otherwise, if a solution pool is added with an existing name an error occurs. The pool manager always has an active pool. The pool manager has the same API as a solution pool, and the methods and data of the active pool are exposed to the user through the pool manager. The active pool defaults to the pool that was most recently added to the pool manager. Solution Objects ---------------- Each :py:class:`Solution` object contains information about the objective and variables. Solutions can be sorted based on their variable values: .. doctest:: :skipif: not glpk_available >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk", abs_opt_gap=0.0) >>> print( [soln.objective().value for soln in solns] ) [70.0, 70.0] >>> sorted_solns = list(sorted(solns)) >>> for soln in sorted_solns: ... print(soln) { "id": 1, "objectives": [ { "index": 0, "name": "o", "suffix": {}, "value": 70.0 } ], "suffix": {}, "variables": [ { "discrete": true, "fixed": false, "index": 0, "name": "x[0]", "suffix": {}, "value": 0 }, { "discrete": true, "fixed": false, "index": 1, "name": "x[1]", "suffix": {}, "value": 1 }, { "discrete": true, "fixed": false, "index": 2, "name": "x[2]", "suffix": {}, "value": 1 }, { "discrete": true, "fixed": false, "index": 3, "name": "x[3]", "suffix": {}, "value": 0 } ] } { "id": 0, "objectives": [ { "index": 0, "name": "o", "suffix": {}, "value": 70.0 } ], "suffix": {}, "variables": [ { "discrete": true, "fixed": false, "index": 0, "name": "x[0]", "suffix": {}, "value": 1 }, { "discrete": true, "fixed": false, "index": 1, "name": "x[1]", "suffix": {}, "value": 0 }, { "discrete": true, "fixed": false, "index": 2, "name": "x[2]", "suffix": {}, "value": 0 }, { "discrete": true, "fixed": false, "index": 3, "name": "x[3]", "suffix": {}, "value": 1 } ] } Further, variable and objective values can be retrieved either using an integer index or the corresponding name: .. doctest:: :skipif: not glpk_available >>> soln = sorted_solns[0] >>> print(f"{soln.objective().value=} {soln.objective(0).value=} {soln.objective('o').value=}") soln.objective().value=70.0 soln.objective(0).value=70.0 soln.objective('o').value=70.0 >>> print(f"{soln.variable(0).value=} {soln.variable('x[0]').value=}") soln.variable(0).value=0 soln.variable('x[0]').value=0