Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
79bcd07
Renaming warn() to warning()
whart222 Jul 1, 2025
50c4ea4
Renaming solnpool.py to gurobi_solnpool.py
whart222 Jul 1, 2025
e50aadf
Renaming test file
whart222 Jul 1, 2025
789ac79
Pulling-in solution pool logic from forestlib
whart222 Jul 1, 2025
39f386d
Rework of solnpools for Balas
whart222 Jul 1, 2025
1f419b7
Integration of pool managers
whart222 Jul 2, 2025
4dc67f3
Removing index_to_variable maps
whart222 Jul 2, 2025
f749087
Rounding discrete values
whart222 Jul 2, 2025
4d789cc
Misc API changes
whart222 Jul 2, 2025
a2c7ba2
Merge branch 'Pyomo:main' into solnpool
whart222 Jul 9, 2025
ab50d29
Reformatting
whart222 Jul 9, 2025
96de282
Refining Bunch API to align with Munch
whart222 Jul 9, 2025
d7ea2ef
Isolating use of "Munch"
whart222 Jul 9, 2025
cafd3a6
Removing import of munch
whart222 Jul 9, 2025
ed7b154
Removing munch import
whart222 Jul 9, 2025
5299493
Rework of dataclass setup
whart222 Jul 9, 2025
6eeb219
Further update to the dataclass
whart222 Jul 9, 2025
fd371a6
Conditional use of dataclass options
whart222 Jul 9, 2025
4ea2d9b
Reformatting with black
whart222 Jul 9, 2025
b80c1bb
Add comparison methods for solutions
whart222 Jul 9, 2025
13e6853
Fixing AOS doctests
whart222 Jul 9, 2025
f638889
Several test fixes
whart222 Jul 9, 2025
834cd95
Reformatting
whart222 Jul 10, 2025
235b702
Added num_solution checks to balas
viens-code Aug 18, 2025
d1668b5
Added num_solution checks to lp_enum
viens-code Aug 18, 2025
9548607
Add num_solution checks to lp_enum_solnpool
viens-code Aug 18, 2025
ac9f517
Updated gurobi_solnpool to check num_solutions and allow PoolSearchMo…
viens-code Aug 18, 2025
1a83132
Added checks to SolutionPool where max_pool_size exists
viens-code Aug 18, 2025
fe41db1
Added tests for invalid policies in SolutionPool
viens-code Aug 18, 2025
706c8db
Added pool name methods to PoolManager
viens-code Aug 18, 2025
14a33bd
Added policy type to SolutionPoolBase
viens-code Aug 18, 2025
0942029
Added methods to get pool/pools policy and max_pool_sizes
viens-code Aug 18, 2025
2430679
Added get_pool_sizes method
viens-code Aug 18, 2025
de7db76
Changed to .items in dict comprehensions where keys and values needed
viens-code Aug 18, 2025
221be05
Readability tweaks to emphasize active pool and set of pools
viens-code Aug 18, 2025
73fd086
Documentation adds for SolutionPool methods
viens-code Aug 19, 2025
249188e
Documentation Updates
viens-code Sep 2, 2025
09b4d66
Updates sense information in KeepBest pool
viens-code Sep 2, 2025
ec110a0
Enforce pass through behavior with PoolManager to_dict method
viens-code Sep 2, 2025
3e12b37
Added PoolManager to_dict pass through test
viens-code Sep 2, 2025
217bdc6
SolutionPool Updates
viens-code Sep 2, 2025
6262edc
Fixed issues caused by absence of to_dict method in Bunch/MyMunch
viens-code Sep 4, 2025
84ec9d0
Merge remote-tracking branch 'origin/main' into solnpool
whart222 Sep 5, 2025
5f4cdbd
Merge branch 'or-fusion:solnpool' into solnpool
viens-code Sep 5, 2025
73f8567
Merge pull request #1 from viens-code/solnpool
viens-code Sep 5, 2025
c0be0c5
Various updates
whart222 Sep 19, 2025
11894e2
Doc update
whart222 Sep 19, 2025
5a8818d
Reworking AOS documentation
whart222 Sep 19, 2025
57df61a
Merge branch 'Pyomo:main' into solnpool
whart222 Sep 19, 2025
0d79e42
Fixing typos
whart222 Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 258 additions & 132 deletions doc/OnlineDocs/explanation/analysis/alternative_solutions.rst

Large diffs are not rendered by default.

61 changes: 36 additions & 25 deletions pyomo/common/collections/bunch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# the U.S. Government retains certain rights in this software.
# ___________________________________________________________________________

import types
import shlex
from collections.abc import Mapping

Expand All @@ -36,31 +37,38 @@ class Bunch(dict):
def __init__(self, *args, **kw):
self._name_ = self.__class__.__name__
for arg in args:
if not isinstance(arg, str):
raise TypeError("Bunch() positional arguments must be strings")
for item in shlex.split(arg):
item = item.split('=', 1)
if len(item) != 2:
raise ValueError(
"Bunch() positional arguments must be space separated "
f"strings of form 'key=value', got '{item[0]}'"
)

# Historically, this used 'exec'. That is unsafe in
# this context (because anyone can pass arguments to a
# Bunch). While not strictly backwards compatible,
# Pyomo was not using this for anything past parsing
# None/float/int values. We will explicitly parse those
# values
try:
val = float(item[1])
if int(val) == val:
val = int(val)
item[1] = val
except:
if item[1].strip() == 'None':
item[1] = None
self[item[0]] = item[1]
if isinstance(arg, types.GeneratorType):
for k, v in arg:
self[k] = v
elif isinstance(arg, str):
for item in shlex.split(arg):
item = item.split('=', 1)
if len(item) != 2:
raise ValueError(
"Bunch() positional arguments must be space separated "
f"strings of form 'key=value', got '{item[0]}'"
)

# Historically, this used 'exec'. That is unsafe in
# this context (because anyone can pass arguments to a
# Bunch). While not strictly backwards compatible,
# Pyomo was not using this for anything past parsing
# None/float/int values. We will explicitly parse those
# values
try:
val = float(item[1])
if int(val) == val:
val = int(val)
item[1] = val
except:
if item[1].strip() == 'None':
item[1] = None
self[item[0]] = item[1]
else:
raise TypeError(
"Bunch() positional arguments must either by generators returning tuples defining a dictionary, or "
"space separated strings of form 'key=value'"
)
for k, v in kw.items():
self[k] = v

Expand Down Expand Up @@ -162,3 +170,6 @@ def __str__(self, nesting=0, indent=''):
attrs.append("".join(text))
attrs.sort()
return "\n".join(attrs)

def toDict(self):
return self
16 changes: 15 additions & 1 deletion pyomo/common/tests/test_bunch.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ def test_Bunch1(self):
)

with self.assertRaisesRegex(
TypeError, r"Bunch\(\) positional arguments must be strings"
TypeError,
r"Bunch\(\) positional arguments must either by generators returning tuples defining a dictionary, or space separated strings of form 'key=value'",
):
Bunch(5)

Expand All @@ -96,6 +97,19 @@ def test_Bunch1(self):
):
Bunch('a=5 foo = 6')

def test_Bunch2(self):
data = dict(a=None, c='d', e="1 2 3", f=" 5 ", foo=1, bar='x')
o1 = Bunch((k, v) for k, v in data.items())
self.assertEqual(
str(o1),
"""a: None
bar: 'x'
c: 'd'
e: '1 2 3'
f: ' 5 '
foo: 1""",
)

def test_pickle(self):
o1 = Bunch('a=None c=d e="1 2 3"', foo=1, bar='x')
s = pickle.dumps(o1)
Expand Down
15 changes: 13 additions & 2 deletions pyomo/contrib/alternative_solutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@
# ___________________________________________________________________________

from pyomo.contrib.alternative_solutions.aos_utils import logcontext
from pyomo.contrib.alternative_solutions.solution import Solution
from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions
from pyomo.contrib.alternative_solutions.solution import (
PyomoSolution,
Solution,
VariableInfo,
ObjectiveInfo,
)
from pyomo.contrib.alternative_solutions.solnpool import PoolManager, PyomoPoolManager
from pyomo.contrib.alternative_solutions.balas import enumerate_binary_solutions
from pyomo.contrib.alternative_solutions.obbt import (
obbt_analysis,
obbt_analysis_bounds_and_solutions,
)
from pyomo.contrib.alternative_solutions.lp_enum import enumerate_linear_solutions
from pyomo.contrib.alternative_solutions.gurobi_lp_enum import (
gurobi_enumerate_linear_solutions,
)
from pyomo.contrib.alternative_solutions.gurobi_solnpool import (
gurobi_generate_solutions,
)
22 changes: 21 additions & 1 deletion pyomo/contrib/alternative_solutions/aos_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________

from pyomo.common.collections import Bunch as Munch
import logging
from contextlib import contextmanager

logger = logging.getLogger(__name__)

from contextlib import contextmanager

from pyomo.common.dependencies import numpy as numpy, numpy_available

Expand Down Expand Up @@ -302,3 +303,22 @@ def get_model_variables(
)

return variable_set


class MyMunch(Munch):
# WEH, MPV needed to add a to_dict since Bunch did not have one
def to_dict(self):
return _to_dict(self)


def _to_dict(x):
xtype = type(x)
if xtype in [float, int, complex, str, list, bool] or x is None:
return x
elif xtype in [tuple, set, frozenset]:
return list(x)
elif xtype in [dict, Munch, MyMunch]:
return {k: _to_dict(v) for k, v in x.items()}
else:
print(f'Here: {x=} {type(x)}')
return x.to_dict()
35 changes: 22 additions & 13 deletions pyomo/contrib/alternative_solutions/balas.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import pyomo.environ as pyo
from pyomo.common.collections import ComponentSet
from pyomo.contrib.alternative_solutions import Solution
from pyomo.contrib.alternative_solutions import PyomoPoolManager
import pyomo.contrib.alternative_solutions.aos_utils as aos_utils


Expand All @@ -31,6 +31,7 @@ def enumerate_binary_solutions(
solver_options={},
tee=False,
seed=None,
poolmanager=None,
):
"""
Finds alternative optimal solutions for a binary problem using no-good
Expand All @@ -44,7 +45,7 @@ def enumerate_binary_solutions(
model : ConcreteModel
A concrete Pyomo model
num_solutions : int
The maximum number of solutions to generate.
The maximum number of solutions to generate. Must be positive
variables: None or a collection of Pyomo _GeneralVarData variables
The variables for which bounds will be generated. None indicates
that all variables will be included. Alternatively, a collection of
Expand All @@ -71,16 +72,21 @@ def enumerate_binary_solutions(
Boolean indicating that the solver output should be displayed.
seed : int
Optional integer seed for the numpy random number generator
poolmanager : None
Optional pool manager that will be used to collect solution

Returns
-------
solutions
A list of Solution objects.
[Solution]
poolmanager
A PyomoPoolManager object

"""
logger.info("STARTING NO-GOOD CUT ANALYSIS")

assert num_solutions >= 1, "num_solutions must be positive integer"
if num_solutions == 1:
logger.warning("Running alternative_solutions method to find only 1 solution!")

assert search_mode in [
"optimal",
"random",
Expand All @@ -90,6 +96,10 @@ def enumerate_binary_solutions(
if seed is not None:
aos_utils._set_numpy_rng(seed)

if poolmanager is None:
poolmanager = PyomoPoolManager()
poolmanager.add_pool(name="enumerate_binary_solutions", policy="keep_all")

all_variables = aos_utils.get_model_variables(model, include_fixed=True)
if variables == None:
binary_variables = [
Expand All @@ -108,18 +118,18 @@ def enumerate_binary_solutions(
else: # pragma: no cover
non_binary_variables.append(var.name)
if len(non_binary_variables) > 0:
logger.warn(
logger.warning(
(
"Warning: The following non-binary variables were included"
"in the variable list and will be ignored:"
)
)
logger.warn(", ".join(non_binary_variables))
logger.warning(", ".join(non_binary_variables))

orig_objective = aos_utils.get_active_objective(model)

if len(binary_variables) == 0:
logger.warn("No binary variables found!")
logger.warning("No binary variables found!")

#
# Setup solver
Expand Down Expand Up @@ -152,7 +162,6 @@ def enumerate_binary_solutions(
else:
opt.update_config.check_for_new_objective = False
opt.update_config.update_objective = False

#
# Initial solve of the model
#
Expand All @@ -172,12 +181,12 @@ def enumerate_binary_solutions(
model.solutions.load_from(results)
orig_objective_value = pyo.value(orig_objective)
logger.info("Found optimal solution, value = {}.".format(orig_objective_value))
solutions = [Solution(model, all_variables, objective=orig_objective)]
poolmanager.add(variables=all_variables, objective=orig_objective)
#
# Return just this solution if there are no binary variables
#
if len(binary_variables) == 0:
return solutions
return poolmanager

aos_block = aos_utils._add_aos_block(model, name="_balas")
logger.info("Added block {} to the model.".format(aos_block))
Expand Down Expand Up @@ -231,7 +240,7 @@ def enumerate_binary_solutions(
logger.info(
"Iteration {}: objective = {}".format(solution_number, orig_obj_value)
)
solutions.append(Solution(model, all_variables, objective=orig_objective))
poolmanager.add(variables=all_variables, objective=orig_objective)
solution_number += 1
elif (
condition == pyo.TerminationCondition.infeasibleOrUnbounded
Expand All @@ -257,4 +266,4 @@ def enumerate_binary_solutions(

logger.info("COMPLETED NO-GOOD CUT ANALYSIS")

return solutions
return poolmanager
Loading
Loading