From 168d7beef2b686a2c10d7abcac78184ac0b4786c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 28 Jul 2023 14:42:14 -0600 Subject: [PATCH 0001/1178] Design discussion: Solver refactor - APPSI review --- pyomo/contrib/appsi/base.py | 167 ++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index ca7255d5628..00f8982349c 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -42,14 +42,42 @@ from pyomo.core.expr.numvalue import NumericConstant +# # TerminationCondition + +# We currently have: Termination condition, solver status, and solution status. +# LL: Michael was trying to go for simplicity. All three conditions can be confusing. +# It is likely okay to have termination condition and solver status. + +# ## Open Questions (User Perspective) +# - Did I (the user) get a reasonable answer back from the solver? +# - If the answer is not reasonable, can I figure out why? + +# ## Our Goal +# Solvers normally tell you what they did and hope the users understand that. +# *We* want to try to return that information but also _help_ the user. + +# ## Proposals +# PROPOSAL 1: PyomoCondition and SolverCondition +# - SolverCondition: what the solver said +# - PyomoCondition: what we interpret that the solver said + +# PROPOSAL 2: TerminationCondition contains... +# - Some finite list of conditions +# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? +# - Replace `optimal` with `normal` or `ok` for the termination flag; `optimal` can be used differently for the solver flag +# - You can use something else like `local`, `global`, `feasible` for solution status + class TerminationCondition(enum.Enum): """ An enumeration for checking the termination condition of solvers """ - unknown = 0 + unknown = 42 """unknown serves as both a default value, and it is used when no other enum member makes sense""" + ok = 0 + """The solver exited with the optimal solution""" + maxTimeLimit = 1 """The solver exited due to a time limit""" @@ -62,46 +90,78 @@ class TerminationCondition(enum.Enum): minStepLength = 4 """The solver exited due to a minimum step length""" - optimal = 5 - """The solver exited with the optimal solution""" - - unbounded = 8 + unbounded = 5 """The solver exited because the problem is unbounded""" - infeasible = 9 + infeasible = 6 """The solver exited because the problem is infeasible""" - infeasibleOrUnbounded = 10 + infeasibleOrUnbounded = 7 """The solver exited because the problem is either infeasible or unbounded""" - error = 11 + error = 8 """The solver exited due to an error""" - interrupted = 12 + interrupted = 9 """The solver exited because it was interrupted""" - licensingProblems = 13 + licensingProblems = 10 """The solver exited due to licensing problems""" -class SolverConfig(ConfigDict): +class SolutionStatus(enum.Enum): + # We may want to not use enum.Enum; we may want to use the flavor that allows sets + noSolution = 0 + locallyOptimal = 1 + globallyOptimal = 2 + feasible = 3 + + +# # SolverConfig + +# The idea here (currently / in theory) is that a call to solve will have a keyword argument `solver_config`: +# ``` +# solve(model, solver_config=...) +# config = self.config(solver_config) +# ``` + +# We have several flavors of options: +# - Solver options +# - Standardized options +# - Wrapper options +# - Interface options +# - potentially... more? + +# ## The Options + +# There are three basic structures: flat, doubly-nested, separate dicts. +# We need to pick between these three structures (and stick with it). + +# **Flat: Clear interface; ambiguous about what goes where; better solve interface.** <- WINNER +# Doubly: More obscure interface; less ambiguity; better programmatic interface. +# SepDicts: Clear delineation; **kwargs becomes confusing (what maps to what?) (NOT HAPPENING) + + +class InterfaceConfig(ConfigDict): """ Attributes ---------- - time_limit: float + time_limit: float - sent to solver Time limit for the solver - stream_solver: bool + stream_solver: bool - wrapper If True, then the solver log goes to stdout - load_solution: bool + load_solution: bool - wrapper If False, then the values of the primal variables will not be loaded into the model - symbolic_solver_labels: bool + symbolic_solver_labels: bool - sent to solver If True, the names given to the solver will reflect the names of the pyomo components. Cannot be changed after set_instance is called. - report_timing: bool + report_timing: bool - wrapper If True, then some timing information will be printed at the end of the solve. + solver_options: ConfigDict or dict + The "raw" solver options to be passed to the solver. """ def __init__( @@ -112,7 +172,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(SolverConfig, self).__init__( + super(InterfaceConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -120,20 +180,19 @@ def __init__( visibility=visibility, ) - self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) self.declare('stream_solver', ConfigValue(domain=bool)) self.declare('load_solution', ConfigValue(domain=bool)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) - self.time_limit: Optional[float] = None + self.time_limit: Optional[float] = self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) self.stream_solver: bool = False self.load_solution: bool = True self.symbolic_solver_labels: bool = False self.report_timing: bool = False -class MIPSolverConfig(SolverConfig): +class MIPSolverConfig(InterfaceConfig): """ Attributes ---------- @@ -167,6 +226,21 @@ def __init__( self.relax_integrality: bool = False +# # SolutionLoaderBase + +# This is an attempt to answer the issue of persistent/non-persistent solution +# loading. This is an attribute of the results object (not the solver). + +# You wouldn't ask the solver to load a solution into a model. You would +# ask the result to load the solution - into the model you solved. +# The results object points to relevant elements; elements do NOT point to +# the results object. + +# Per Michael: This may be a bit clunky; but it works. +# Per Siirola: We may want to rethink `load_vars` and `get_primals`. In particular, +# this is for efficiency - don't create a dictionary you don't need to. And what is +# the client use-case for `get_primals`? + class SolutionLoaderBase(abc.ABC): def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -584,6 +658,46 @@ def __init__( self.treat_fixed_vars_as_params: bool = True +# # Solver + +# ## Open Question: What does 'solve' look like? + +# We may want to use the 80/20 rule here - we support 80% of the cases; anything +# fancier than that is going to require "writing code." The 80% would be offerings +# that are supported as part of the `pyomo` script. + +# ## Configs + +# We will likely have two configs for `solve`: standardized config (processes `**kwargs`) +# and implicit ConfigDict with some specialized options. + +# These have to be separated because there is a set that need to be passed +# directly to the solver. The other is Pyomo options / our standardized options +# (a few of which might be passed directly to solver, e.g., time_limit). + +# ## Contained Methods + +# We do not like `symbol_map`; it's keyed towards file-based interfaces. That +# is the `lp` writer; the `nl` writer doesn't need that (and in fact, it's +# obnoxious). The new `nl` writer returns back more meaningful things to the `nl` +# interface. + +# If the writer needs a symbol map, it will return it. But it is _not_ a +# solver thing. So it does not need to continue to exist in the solver interface. + +# All other options are reasonable. + +# ## Other (maybe should be contained) Methods + +# There are other methods in other solvers such as `warmstart`, `sos`; do we +# want to continue to support and/or offer those features? + +# The solver interface is not responsible for telling the client what +# it can do, e.g., `supports_sos2`. This is actually a contract between +# the solver and its writer. + +# End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). + class Solver(abc.ABC): class Availability(enum.IntEnum): NotFound = 0 @@ -610,7 +724,7 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: _BlockData, tee = False, timer: HierarchicalTimer = None, **kwargs) -> Results: """ Solve a Pyomo model. @@ -618,8 +732,12 @@ def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: ---------- model: _BlockData The Pyomo model to be solved + tee: bool + Show solver output in the terminal timer: HierarchicalTimer An option timer for reporting timing + **kwargs + Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) Returns ------- @@ -672,17 +790,12 @@ def config(self): Returns ------- - SolverConfig + InterfaceConfig An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ pass - @property - @abc.abstractmethod - def symbol_map(self): - pass - def is_persistent(self): """ Returns From 55ee816fc8247750ee049a8c5ddec91aa47f204b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 10:44:30 -0600 Subject: [PATCH 0002/1178] Finish conversion from optimal to ok --- pyomo/contrib/appsi/base.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 00f8982349c..9ab4d5020a7 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -14,7 +14,7 @@ from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective @@ -36,7 +36,6 @@ ) from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap -import weakref from .cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant @@ -460,7 +459,7 @@ class Results(object): >>> opt = appsi.solvers.Ipopt() >>> opt.config.load_solution = False >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.optimal: #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.ok: #doctest:+SKIP ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP ... results.solution_loader.load_vars() #doctest:+SKIP ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP @@ -1583,7 +1582,7 @@ def update(self, timer: HierarchicalTimer = None): TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.optimal: LegacyTerminationCondition.optimal, + TerminationCondition.ok: LegacyTerminationCondition.optimal, TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, @@ -1599,7 +1598,7 @@ def update(self, timer: HierarchicalTimer = None): TerminationCondition.maxIterations: LegacySolverStatus.aborted, TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.optimal: LegacySolverStatus.ok, + TerminationCondition.ok: LegacySolverStatus.ok, TerminationCondition.unbounded: LegacySolverStatus.error, TerminationCondition.infeasible: LegacySolverStatus.error, TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, @@ -1615,7 +1614,7 @@ def update(self, timer: HierarchicalTimer = None): TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.optimal: LegacySolutionStatus.optimal, + TerminationCondition.ok: LegacySolutionStatus.optimal, TerminationCondition.unbounded: LegacySolutionStatus.unbounded, TerminationCondition.infeasible: LegacySolutionStatus.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, From a30477ce113b4fe9d87c4fd29b797542da14d8fe Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 10:58:15 -0600 Subject: [PATCH 0003/1178] Change call to InterfaceConfig --- pyomo/contrib/appsi/base.py | 6 +++--- pyomo/contrib/appsi/solvers/cbc.py | 4 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 4 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 9ab4d5020a7..630aefbbd29 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -116,7 +116,7 @@ class SolutionStatus(enum.Enum): feasible = 3 -# # SolverConfig +# # InterfaceConfig # The idea here (currently / in theory) is that a call to solve will have a keyword argument `solver_config`: # ``` @@ -191,7 +191,7 @@ def __init__( self.report_timing: bool = False -class MIPSolverConfig(InterfaceConfig): +class MIPInterfaceConfig(InterfaceConfig): """ Attributes ---------- @@ -210,7 +210,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(MIPSolverConfig, self).__init__( + super(MIPInterfaceConfig, self).__init__( description=description, doc=doc, implicit=implicit, diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index b31a96dbf8a..833ef54b2cf 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -4,7 +4,7 @@ PersistentSolver, Results, TerminationCondition, - SolverConfig, + InterfaceConfig, PersistentSolutionLoader, ) from pyomo.contrib.appsi.writers import LPWriter @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) -class CbcConfig(SolverConfig): +class CbcConfig(InterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 6c5e281ffac..7b51d6611c2 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -3,7 +3,7 @@ PersistentSolver, Results, TerminationCondition, - MIPSolverConfig, + MIPInterfaceConfig, PersistentSolutionLoader, ) from pyomo.contrib.appsi.writers import LPWriter @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) -class CplexConfig(MIPSolverConfig): +class CplexConfig(MIPInterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 2362612e9ee..0d99089fbab 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ PersistentSolver, Results, TerminationCondition, - MIPSolverConfig, + MIPInterfaceConfig, PersistentBase, PersistentSolutionLoader, ) @@ -53,7 +53,7 @@ class DegreeError(PyomoException): pass -class GurobiConfig(MIPSolverConfig): +class GurobiConfig(MIPInterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 9de5accfb91..63c799b0f61 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,7 +20,7 @@ PersistentSolver, Results, TerminationCondition, - MIPSolverConfig, + MIPInterfaceConfig, PersistentBase, PersistentSolutionLoader, ) @@ -38,7 +38,7 @@ class DegreeError(PyomoException): pass -class HighsConfig(MIPSolverConfig): +class HighsConfig(MIPInterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index fde4c55073d..047ce09a533 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -4,7 +4,7 @@ PersistentSolver, Results, TerminationCondition, - SolverConfig, + InterfaceConfig, PersistentSolutionLoader, ) from pyomo.contrib.appsi.writers import NLWriter @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) -class IpoptConfig(SolverConfig): +class IpoptConfig(InterfaceConfig): def __init__( self, description=None, From 75cc8c4c654d0c15a7d0920c9b990eece9f99f6f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 11:24:57 -0600 Subject: [PATCH 0004/1178] Change test checks --- pyomo/contrib/appsi/base.py | 15 ++++-- .../contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 10 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 +- pyomo/contrib/appsi/solvers/highs.py | 6 +-- pyomo/contrib/appsi/solvers/ipopt.py | 10 ++-- .../solvers/tests/test_gurobi_persistent.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 50 +++++++++---------- 9 files changed, 55 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 630aefbbd29..fe0b3ee2999 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -62,10 +62,11 @@ # PROPOSAL 2: TerminationCondition contains... # - Some finite list of conditions -# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? +# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? # - Replace `optimal` with `normal` or `ok` for the termination flag; `optimal` can be used differently for the solver flag # - You can use something else like `local`, `global`, `feasible` for solution status + class TerminationCondition(enum.Enum): """ An enumeration for checking the termination condition of solvers @@ -184,7 +185,9 @@ def __init__( self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) - self.time_limit: Optional[float] = self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) + self.time_limit: Optional[float] = self.declare( + 'time_limit', ConfigValue(domain=NonNegativeFloat) + ) self.stream_solver: bool = False self.load_solution: bool = True self.symbolic_solver_labels: bool = False @@ -240,6 +243,7 @@ def __init__( # this is for efficiency - don't create a dictionary you don't need to. And what is # the client use-case for `get_primals`? + class SolutionLoaderBase(abc.ABC): def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -689,7 +693,7 @@ def __init__( # ## Other (maybe should be contained) Methods # There are other methods in other solvers such as `warmstart`, `sos`; do we -# want to continue to support and/or offer those features? +# want to continue to support and/or offer those features? # The solver interface is not responsible for telling the client what # it can do, e.g., `supports_sos2`. This is actually a contract between @@ -697,6 +701,7 @@ def __init__( # End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). + class Solver(abc.ABC): class Availability(enum.IntEnum): NotFound = 0 @@ -723,7 +728,9 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, tee = False, timer: HierarchicalTimer = None, **kwargs) -> Results: + def solve( + self, model: _BlockData, tee=False, timer: HierarchicalTimer = None, **kwargs + ) -> Results: """ Solve a Pyomo model. diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index de22d28e0a4..5cbac7c81e3 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -31,7 +31,7 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == appsi.base.TerminationCondition.optimal + assert res.termination_condition == appsi.base.TerminationCondition.ok obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 833ef54b2cf..641a90c3ae7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -232,7 +232,7 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible @@ -307,7 +307,7 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition == TerminationCondition.optimal + results.termination_condition == TerminationCondition.ok and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): @@ -316,7 +316,7 @@ def _parse_soln(self): results.best_feasible_objective = None else: results.best_feasible_objective = obj_val - elif results.termination_condition == TerminationCondition.optimal: + elif results.termination_condition == TerminationCondition.ok: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -451,7 +451,7 @@ def get_duals(self, cons_to_load=None): if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -469,7 +469,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 7b51d6611c2..2ea051c58b4 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -284,7 +284,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: @@ -336,7 +336,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'results.best_feasible_objective before loading a solution.' ) else: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition != TerminationCondition.ok: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 0d99089fbab..af17a398845 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -874,7 +874,7 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible elif status == grb.INF_OR_UNBD: @@ -925,7 +925,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition != TerminationCondition.ok: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 63c799b0f61..8de873635a5 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -610,7 +610,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif status == highspy.HighsModelStatus.kInfeasible: results.termination_condition = TerminationCondition.infeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: @@ -633,7 +633,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if results.termination_condition == TerminationCondition.optimal: + if results.termination_condition == TerminationCondition.ok: has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, @@ -645,7 +645,7 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition != TerminationCondition.ok: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 047ce09a533..2d8cfe40b32 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -303,7 +303,7 @@ def _parse_sol(self): termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif 'Problem may be infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible elif 'problem might be unbounded' in termination_line: @@ -384,7 +384,7 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition == TerminationCondition.optimal + results.termination_condition == TerminationCondition.ok and self.config.load_solution ): for v, val in self._primal_sol.items(): @@ -395,7 +395,7 @@ def _parse_sol(self): results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif results.termination_condition == TerminationCondition.optimal: + elif results.termination_condition == TerminationCondition.ok: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -526,7 +526,7 @@ def get_duals( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -544,7 +544,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 6366077642d..03042bbb5f4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -160,7 +160,7 @@ def test_lp(self): res = opt.solve(self.m) self.assertAlmostEqual(x + y, res.best_feasible_objective) self.assertAlmostEqual(x + y, res.best_objective_bound) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index bafccb3527c..88a278bfe3b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -122,13 +122,13 @@ def test_range_constraint(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -143,7 +143,7 @@ def test_reduced_costs(self, name: str, opt_class: Type[PersistentSolver]): m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -159,13 +159,13 @@ def test_reduced_costs2(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -193,7 +193,7 @@ def test_param_changes(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -229,7 +229,7 @@ def test_immutable_param(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -261,7 +261,7 @@ def test_equality(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -299,7 +299,7 @@ def test_linear_expression(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) @@ -327,7 +327,7 @@ def test_no_objective(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) @@ -354,7 +354,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[PersistentSolver]): m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -365,7 +365,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[PersistentSolver]): m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -377,7 +377,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[PersistentSolver]): del m.c3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -401,7 +401,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[PersistentSolver]): res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual(res.termination_condition, TerminationCondition.optimal) + self.assertNotEqual(res.termination_condition, TerminationCondition.ok) if opt_class is Ipopt: acceptable_termination_conditions = { TerminationCondition.infeasible, @@ -685,7 +685,7 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) @@ -720,7 +720,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[PersistentSolver]) opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -733,7 +733,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[PersistentSolver]) opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -741,7 +741,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[PersistentSolver]) opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -800,7 +800,7 @@ def test_with_numpy(self, name: str, opt_class: Type[PersistentSolver]): ) ) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -1126,14 +1126,14 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]) m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @@ -1156,7 +1156,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1166,7 +1166,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1188,12 +1188,12 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolver]): m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 3) From e4b9313f417d177125b66a9ff0576ac0a70e334a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 13:59:12 -0600 Subject: [PATCH 0005/1178] Update docs --- pyomo/contrib/appsi/base.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index fe0b3ee2999..e4e6a915cbd 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -110,10 +110,22 @@ class TerminationCondition(enum.Enum): class SolutionStatus(enum.Enum): - # We may want to not use enum.Enum; we may want to use the flavor that allows sets + """ + An enumeration for interpreting the result of a termination + + TODO: We may want to not use enum.Enum; we may want to use the flavor that allows sets + """ + + """No solution found""" noSolution = 0 + + """Locally optimal solution identified""" locallyOptimal = 1 + + """Globally optimal solution identified""" globallyOptimal = 2 + + """Feasible solution identified""" feasible = 3 @@ -160,8 +172,6 @@ class InterfaceConfig(ConfigDict): report_timing: bool - wrapper If True, then some timing information will be printed at the end of the solve. - solver_options: ConfigDict or dict - The "raw" solver options to be passed to the solver. """ def __init__( From 4c9af82c7dd2226412de5ca50ed8c85be66cc67f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 11 Aug 2023 10:12:40 -0600 Subject: [PATCH 0006/1178] Adjust tests to check for 'ok' instead of 'optimal' --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 4a5c816394f..ec9f397bdc4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -91,7 +91,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -99,7 +99,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) From cdcfeff95efc8881376986c031bb7b0357b3c3ef Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 11 Aug 2023 12:55:34 -0600 Subject: [PATCH 0007/1178] Much discussion with @jsiirola resulted in a small change --- pyomo/contrib/appsi/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index fd8f432a7fa..a715fabf1f6 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -75,7 +75,7 @@ class TerminationCondition(enum.Enum): unknown = 42 """unknown serves as both a default value, and it is used when no other enum member makes sense""" - ok = 0 + convergenceCriteriaSatisfied = 0 """The solver exited with the optimal solution""" maxTimeLimit = 1 From 92480c386a23e692eaac9bee3097ce9fce1fdd3b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 15 Aug 2023 11:13:38 -0600 Subject: [PATCH 0008/1178] Small refactor in APPSI to improve performance; small rework of TerminationCondition/SolverStatus --- pyomo/contrib/appsi/base.py | 200 ++++++++---------- pyomo/contrib/appsi/build.py | 4 +- .../contrib/appsi/examples/getting_started.py | 4 +- .../appsi/examples/tests/test_examples.py | 2 +- pyomo/contrib/appsi/fbbt.py | 16 +- pyomo/contrib/appsi/solvers/cbc.py | 18 +- pyomo/contrib/appsi/solvers/cplex.py | 10 +- pyomo/contrib/appsi/solvers/gurobi.py | 66 +++--- pyomo/contrib/appsi/solvers/highs.py | 70 +++--- pyomo/contrib/appsi/solvers/ipopt.py | 8 +- .../solvers/tests/test_gurobi_persistent.py | 2 +- .../solvers/tests/test_ipopt_persistent.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 8 +- pyomo/contrib/appsi/tests/test_base.py | 8 +- pyomo/contrib/appsi/tests/test_interval.py | 4 +- .../utils/collect_vars_and_named_exprs.py | 8 +- pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/lp_writer.py | 14 +- pyomo/contrib/appsi/writers/nl_writer.py | 18 +- .../appsi/writers/tests/test_nl_writer.py | 2 +- 20 files changed, 227 insertions(+), 239 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index a715fabf1f6..805e96dacf1 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -72,61 +72,66 @@ class TerminationCondition(enum.Enum): An enumeration for checking the termination condition of solvers """ - unknown = 42 """unknown serves as both a default value, and it is used when no other enum member makes sense""" + unknown = 42 + """The solver exited because the convergence criteria were satisfied""" convergenceCriteriaSatisfied = 0 - """The solver exited with the optimal solution""" - maxTimeLimit = 1 """The solver exited due to a time limit""" + maxTimeLimit = 1 - maxIterations = 2 - """The solver exited due to an iteration limit """ + """The solver exited due to an iteration limit""" + iterationLimit = 2 - objectiveLimit = 3 """The solver exited due to an objective limit""" + objectiveLimit = 3 - minStepLength = 4 """The solver exited due to a minimum step length""" + minStepLength = 4 - unbounded = 5 """The solver exited because the problem is unbounded""" + unbounded = 5 - infeasible = 6 - """The solver exited because the problem is infeasible""" + """The solver exited because the problem is proven infeasible""" + provenInfeasible = 6 + + """The solver exited because the problem was found to be locally infeasible""" + locallyInfeasible = 7 - infeasibleOrUnbounded = 7 """The solver exited because the problem is either infeasible or unbounded""" + infeasibleOrUnbounded = 8 - error = 8 """The solver exited due to an error""" + error = 9 - interrupted = 9 """The solver exited because it was interrupted""" + interrupted = 10 - licensingProblems = 10 """The solver exited due to licensing problems""" + licensingProblems = 11 -class SolutionStatus(enum.Enum): +class SolutionStatus(enum.IntEnum): """ - An enumeration for interpreting the result of a termination - - TODO: We may want to not use enum.Enum; we may want to use the flavor that allows sets + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + For now, we are choosing to use IntEnum such that return values are numerically + assigned in increasing order. """ - """No solution found""" + """No (single) solution found; possible that a population of solutions was returned""" noSolution = 0 - """Locally optimal solution identified""" - locallyOptimal = 1 - - """Globally optimal solution identified""" - globallyOptimal = 2 + """Solution point does not satisfy some domains and/or constraints""" + infeasible = 10 """Feasible solution identified""" - feasible = 3 + feasible = 20 + + """Optimal solution identified""" + optimal = 30 # # InterfaceConfig @@ -182,7 +187,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(InterfaceConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -223,7 +228,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(MIPInterfaceConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -404,9 +409,9 @@ def get_duals( 'for the given problem type.' ) if cons_to_load is None: - duals = dict(self._duals) + duals = {self._duals} else: - duals = dict() + duals = {} for c in cons_to_load: duals[c] = self._duals[c] return duals @@ -421,9 +426,9 @@ def get_slacks( 'for the given problem type.' ) if cons_to_load is None: - slacks = dict(self._slacks) + slacks = {self._slacks} else: - slacks = dict() + slacks = {} for c in cons_to_load: slacks[c] = self._slacks[c] return slacks @@ -446,7 +451,7 @@ def get_reduced_costs( return rc -class Results(object): +class Results(): """ Attributes ---------- @@ -526,7 +531,7 @@ def __init__( ): if doc is None: doc = 'Configuration options to detect changes in model between solves' - super(UpdateConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -1031,23 +1036,23 @@ def invalidate(self): class PersistentBase(abc.ABC): def __init__(self, only_child_vars=False): self._model = None - self._active_constraints = dict() # maps constraint to (lower, body, upper) - self._vars = dict() # maps var id to (var, lb, ub, fixed, domain, value) - self._params = dict() # maps param id to param + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param self._objective = None self._objective_expr = None self._objective_sense = None self._named_expressions = ( - dict() + {} ) # maps constraint to list of tuples (named_expr, named_expr.expr) self._external_functions = ComponentMap() - self._obj_named_expressions = list() + self._obj_named_expressions = [] self._update_config = UpdateConfig() self._referenced_variables = ( - dict() + {} ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = dict() - self._vars_referenced_by_obj = list() + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] self._expr_types = None self.use_extensions = False self._only_child_vars = only_child_vars @@ -1081,7 +1086,7 @@ def add_variables(self, variables: List[_GeneralVarData]): raise ValueError( 'variable {name} has already been added'.format(name=v.name) ) - self._referenced_variables[id(v)] = [dict(), dict(), None] + self._referenced_variables[id(v)] = [{}, {}, None] self._vars[id(v)] = ( v, v._lb, @@ -1106,24 +1111,24 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): pass def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = dict() + new_vars = {} for v in variables: v_id = id(v) if v_id not in self._referenced_variables: new_vars[v_id] = v - self.add_variables(list(new_vars.values())) + self.add_variables([new_vars.values()]) def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = dict() + vars_to_remove = {} for v in variables: v_id = id(v) ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) + self.remove_variables([vars_to_remove.values()]) def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = dict() + all_fixed_vars = {} for con in cons: if con in self._named_expressions: raise ValueError( @@ -1165,7 +1170,7 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): variables = con.get_variables() if not self._only_child_vars: self._check_for_new_vars(variables) - self._named_expressions[con] = list() + self._named_expressions[con] = [] self._vars_referenced_by_con[con] = variables for v in variables: self._referenced_variables[id(v)][1][con] = None @@ -1206,20 +1211,20 @@ def set_objective(self, obj: _GeneralObjectiveData): for v in fixed_vars: v.fix() else: - self._vars_referenced_by_obj = list() + self._vars_referenced_by_obj = [] self._objective = None self._objective_expr = None self._objective_sense = None - self._obj_named_expressions = list() + self._obj_named_expressions = [] self._set_objective(obj) def add_block(self, block): - param_dict = dict() + param_dict = {} for p in block.component_objects(Param, descend_into=True): if p.mutable: for _p in p.values(): param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) + self.add_params([param_dict.values()]) if self._only_child_vars: self.add_variables( list( @@ -1230,20 +1235,10 @@ def add_block(self, block): ) ) self.add_constraints( - [ - con - for con in block.component_data_objects( - Constraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(Constraint, descend_into=True, active=True)) ) self.add_sos_constraints( - [ - con - for con in block.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) ) obj = get_objective(block) if obj is not None: @@ -1326,20 +1321,10 @@ def remove_params(self, params: List[_ParamData]): def remove_block(self, block): self.remove_constraints( - [ - con - for con in block.component_data_objects( - ctype=Constraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) ) self.remove_sos_constraints( - [ - con - for con in block.component_data_objects( - ctype=SOSConstraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) ) if self._only_child_vars: self.remove_variables( @@ -1387,17 +1372,17 @@ def update(self, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() config = self.update_config - new_vars = list() - old_vars = list() - new_params = list() - old_params = list() - new_cons = list() - old_cons = list() - old_sos = list() - new_sos = list() - current_vars_dict = dict() - current_cons_dict = dict() - current_sos_dict = dict() + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} timer.start('vars') if self._only_child_vars and ( config.check_for_new_or_removed_vars or config.update_vars @@ -1417,7 +1402,7 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('vars') timer.start('params') if config.check_for_new_or_removed_params: - current_params_dict = dict() + current_params_dict = {} for p in self._model.component_objects(Param, descend_into=True): if p.mutable: for _p in p.values(): @@ -1482,11 +1467,11 @@ def update(self, timer: HierarchicalTimer = None): new_cons_set = set(new_cons) new_sos_set = set(new_sos) new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = dict() + cons_to_remove_and_add = {} need_to_set_objective = False if config.update_constraints: - cons_to_update = list() - sos_to_update = list() + cons_to_update = [] + sos_to_update = [] for c in current_cons_dict.keys(): if c not in new_cons_set: cons_to_update.append(c) @@ -1524,7 +1509,7 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('cons') timer.start('vars') if self._only_child_vars and config.update_vars: - vars_to_check = list() + vars_to_check = [] for v_id, v in current_vars_dict.items(): if v_id not in new_vars_set: vars_to_check.append(v) @@ -1532,7 +1517,7 @@ def update(self, timer: HierarchicalTimer = None): end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] if config.update_vars: - vars_to_update = list() + vars_to_update = [] for v in vars_to_check: _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] if lb is not v._lb: @@ -1557,7 +1542,7 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('cons') timer.start('named expressions') if config.update_named_expressions: - cons_to_update = list() + cons_to_update = [] for c, expr_list in self._named_expressions.items(): if c in new_cons_set: continue @@ -1599,12 +1584,13 @@ def update(self, timer: HierarchicalTimer = None): legacy_termination_condition_map = { TerminationCondition.unknown: LegacyTerminationCondition.unknown, TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, - TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.ok: LegacyTerminationCondition.optimal, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, - TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, TerminationCondition.error: LegacyTerminationCondition.error, TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, @@ -1615,12 +1601,13 @@ def update(self, timer: HierarchicalTimer = None): legacy_solver_status_map = { TerminationCondition.unknown: LegacySolverStatus.unknown, TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, - TerminationCondition.maxIterations: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.ok: LegacySolverStatus.ok, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, TerminationCondition.unbounded: LegacySolverStatus.error, - TerminationCondition.infeasible: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, TerminationCondition.error: LegacySolverStatus.error, TerminationCondition.interrupted: LegacySolverStatus.aborted, @@ -1631,12 +1618,13 @@ def update(self, timer: HierarchicalTimer = None): legacy_solution_status_map = { TerminationCondition.unknown: LegacySolutionStatus.unknown, TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.ok: LegacySolutionStatus.optimal, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, TerminationCondition.unbounded: LegacySolutionStatus.unbounded, - TerminationCondition.infeasible: LegacySolutionStatus.infeasible, + TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, + TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, TerminationCondition.error: LegacySolutionStatus.error, TerminationCondition.interrupted: LegacySolutionStatus.error, @@ -1644,7 +1632,7 @@ def update(self, timer: HierarchicalTimer = None): } -class LegacySolverInterface(object): +class LegacySolverInterface(): def solve( self, model: _BlockData, @@ -1683,7 +1671,7 @@ def solve( if options is not None: self.options = options - results: Results = super(LegacySolverInterface, self).solve(model) + results: Results = super().solve(model) legacy_results = LegacySolverResults() legacy_soln = LegacySolution() @@ -1760,7 +1748,7 @@ def solve( return legacy_results def available(self, exception_flag=True): - ans = super(LegacySolverInterface, self).available() + ans = super().available() if exception_flag and not ans: raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') return bool(ans) diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 2a4e7bb785e..6146272978c 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -80,7 +80,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super(appsi_build_ext, self).run() + super().run() if not self.inplace: library = glob.glob("build/*/appsi_cmodel.*")[0] target = os.path.join( @@ -117,7 +117,7 @@ def run(self): pybind11.setup_helpers.MACOS = original_pybind11_setup_helpers_macos -class AppsiBuilder(object): +class AppsiBuilder(): def __call__(self, parallel): return build_appsi() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 5cbac7c81e3..6d2cce76925 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -24,8 +24,8 @@ def main(plot=True, n_points=200): # write a for loop to vary the value of parameter p from 1 to 10 p_values = [float(i) for i in np.linspace(1, 10, n_points)] - obj_values = list() - x_values = list() + obj_values = [] + x_values = [] timer = HierarchicalTimer() # create a timer for some basic profiling timer.start('p loop') for p_val in p_values: diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index d2c88224a7d..ffcecaf0c5f 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,5 +1,5 @@ from pyomo.contrib.appsi.examples import getting_started -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib import appsi diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 92a0e0c8cbc..22badd83d12 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -35,7 +35,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(IntervalConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -62,14 +62,14 @@ def __init__( class IntervalTightener(PersistentBase): def __init__(self): - super(IntervalTightener, self).__init__() + super().__init__() self._config = IntervalConfig() self._cmodel = None - self._var_map = dict() - self._con_map = dict() - self._param_map = dict() - self._rvar_map = dict() - self._rcon_map = dict() + self._var_map = {} + self._con_map = {} + self._param_map = {} + self._rvar_map = {} + self._rcon_map = {} self._pyomo_expr_types = cmodel.PyomoExprTypes() self._symbolic_solver_labels: bool = False self._symbol_map = SymbolMap() @@ -254,7 +254,7 @@ def _update_pyomo_var_bounds(self): self._vars[v_id] = (_v, _lb, cv_ub, _fixed, _domain, _value) def _deactivate_satisfied_cons(self): - cons_to_deactivate = list() + cons_to_deactivate = [] if self.config.deactivate_satisfied_constraints: for c, cc in self._con_map.items(): if not cc.active: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 6fd01fb9149..84a38ec3cdb 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -42,7 +42,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(CbcConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -66,12 +66,12 @@ def __init__( class Cbc(PersistentSolver): def __init__(self, only_child_vars=False): self._config = CbcConfig() - self._solver_options = dict() + self._solver_options = {} self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = dict() - self._primal_sol = dict() - self._reduced_costs = dict() + self._dual_sol = {} + self._primal_sol = {} + self._reduced_costs = {} self._last_results_object: Optional[Results] = None def available(self): @@ -261,9 +261,9 @@ def _parse_soln(self): first_var_line = ndx last_var_line = len(all_lines) - 1 - self._dual_sol = dict() - self._primal_sol = dict() - self._reduced_costs = dict() + self._dual_sol = {} + self._primal_sol = {} + self._reduced_costs = {} symbol_map = self._writer.symbol_map @@ -362,7 +362,7 @@ def _check_and_escape_options(): yield tmp_k, tmp_v cmd = [str(config.executable)] - action_options = list() + action_options = [] if config.time_limit is not None: cmd.extend(['-sec', str(config.time_limit)]) cmd.extend(['-timeMode', 'elapsed']) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index f007573639b..47042586d0b 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -38,7 +38,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(CplexConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -59,7 +59,7 @@ def __init__( class CplexResults(Results): def __init__(self, solver): - super(CplexResults, self).__init__() + super().__init__() self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) @@ -69,7 +69,7 @@ class Cplex(PersistentSolver): def __init__(self, only_child_vars=False): self._config = CplexConfig() - self._solver_options = dict() + self._solver_options = {} self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None self._last_results_object: Optional[CplexResults] = None @@ -400,7 +400,7 @@ def get_duals( con_names = self._cplex_model.linear_constraints.get_names() dual_values = self._cplex_model.solution.get_dual_values() else: - con_names = list() + con_names = [] for con in cons_to_load: orig_name = symbol_map.byObject[id(con)] if con.equality: @@ -412,7 +412,7 @@ def get_duals( con_names.append(orig_name + '_ub') dual_values = self._cplex_model.solution.get_dual_values(con_names) - res = dict() + res = {} for name, val in zip(con_names, dual_values): orig_name = name[:-3] if orig_name == 'obj_const_con': diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 999b542ad70..23a87e06f1c 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -62,7 +62,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(GurobiConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -95,12 +95,12 @@ def get_primals(self, vars_to_load=None, solution_number=0): class GurobiResults(Results): def __init__(self, solver): - super(GurobiResults, self).__init__() + super().__init__() self.wallclock_time = None self.solution_loader = GurobiSolutionLoader(solver=solver) -class _MutableLowerBound(object): +class _MutableLowerBound(): def __init__(self, expr): self.var = None self.expr = expr @@ -109,7 +109,7 @@ def update(self): self.var.setAttr('lb', value(self.expr)) -class _MutableUpperBound(object): +class _MutableUpperBound(): def __init__(self, expr): self.var = None self.expr = expr @@ -118,7 +118,7 @@ def update(self): self.var.setAttr('ub', value(self.expr)) -class _MutableLinearCoefficient(object): +class _MutableLinearCoefficient(): def __init__(self): self.expr = None self.var = None @@ -129,7 +129,7 @@ def update(self): self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) -class _MutableRangeConstant(object): +class _MutableRangeConstant(): def __init__(self): self.lhs_expr = None self.rhs_expr = None @@ -145,7 +145,7 @@ def update(self): slack.ub = rhs_val - lhs_val -class _MutableConstant(object): +class _MutableConstant(): def __init__(self): self.expr = None self.con = None @@ -154,7 +154,7 @@ def update(self): self.con.rhs = value(self.expr) -class _MutableQuadraticConstraint(object): +class _MutableQuadraticConstraint(): def __init__( self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs ): @@ -189,7 +189,7 @@ def get_updated_rhs(self): return value(self.constant.expr) -class _MutableObjective(object): +class _MutableObjective(): def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant = constant @@ -217,7 +217,7 @@ def get_updated_expression(self): return gurobi_expr -class _MutableQuadraticCoefficient(object): +class _MutableQuadraticCoefficient(): def __init__(self): self.expr = None self.var1 = None @@ -233,21 +233,21 @@ class Gurobi(PersistentBase, PersistentSolver): _num_instances = 0 def __init__(self, only_child_vars=False): - super(Gurobi, self).__init__(only_child_vars=only_child_vars) + super().__init__(only_child_vars=only_child_vars) self._num_instances += 1 self._config = GurobiConfig() - self._solver_options = dict() + self._solver_options = {} self._solver_model = None self._symbol_map = SymbolMap() self._labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._pyomo_sos_to_solver_sos_map = dict() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_con_to_pyomo_con_map = {} + self._pyomo_sos_to_solver_sos_map = {} self._range_constraints = OrderedSet() - self._mutable_helpers = dict() - self._mutable_bounds = dict() - self._mutable_quadratic_helpers = dict() + self._mutable_helpers = {} + self._mutable_bounds = {} + self._mutable_quadratic_helpers = {} self._mutable_objective = None self._needs_updated = True self._callback = None @@ -448,12 +448,12 @@ def _process_domain_and_bounds( return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - var_names = list() - vtypes = list() - lbs = list() - ubs = list() - mutable_lbs = dict() - mutable_ubs = dict() + var_names = [] + vtypes = [] + lbs = [] + ubs = [] + mutable_lbs = {} + mutable_ubs = {} for ndx, var in enumerate(variables): varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds( @@ -519,8 +519,8 @@ def set_instance(self, model): self.set_objective(None) def _get_expr_from_pyomo_expr(self, expr): - mutable_linear_coefficients = list() - mutable_quadratic_coefficients = list() + mutable_linear_coefficients = [] + mutable_quadratic_coefficients = [] repn = generate_standard_repn(expr, quadratic=True, compute_values=False) degree = repn.polynomial_degree() @@ -530,7 +530,7 @@ def _get_expr_from_pyomo_expr(self, expr): ) if len(repn.linear_vars) > 0: - linear_coef_vals = list() + linear_coef_vals = [] for ndx, coef in enumerate(repn.linear_coefs): if not is_constant(coef): mutable_linear_coefficient = _MutableLinearCoefficient() @@ -824,8 +824,8 @@ def _set_objective(self, obj): sense = gurobipy.GRB.MINIMIZE gurobi_expr = 0 repn_constant = 0 - mutable_linear_coefficients = list() - mutable_quadratic_coefficients = list() + mutable_linear_coefficients = [] + mutable_quadratic_coefficients = [] else: if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE @@ -1047,7 +1047,7 @@ def get_duals(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - dual = dict() + dual = {} if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() @@ -1090,7 +1090,7 @@ def get_slacks(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - slack = dict() + slack = {} gurobi_range_con_vars = OrderedSet(self._solver_model.getVars()) - OrderedSet( self._pyomo_var_to_solver_var_map.values() @@ -1140,7 +1140,7 @@ def get_slacks(self, cons_to_load=None): def update(self, timer: HierarchicalTimer = None): if self._needs_updated: self._update_gurobi_model() - super(Gurobi, self).update(timer=timer) + super().update(timer=timer) self._update_gurobi_model() def _update_gurobi_model(self): diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 23f49a057a7..528fb2f3087 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -47,7 +47,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(HighsConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -71,7 +71,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class _MutableVarBounds(object): +class _MutableVarBounds(): def __init__(self, lower_expr, upper_expr, pyomo_var_id, var_map, highs): self.pyomo_var_id = pyomo_var_id self.lower_expr = lower_expr @@ -86,7 +86,7 @@ def update(self): self.highs.changeColBounds(col_ndx, lb, ub) -class _MutableLinearCoefficient(object): +class _MutableLinearCoefficient(): def __init__(self, pyomo_con, pyomo_var_id, con_map, var_map, expr, highs): self.expr = expr self.highs = highs @@ -101,7 +101,7 @@ def update(self): self.highs.changeCoeff(row_ndx, col_ndx, value(self.expr)) -class _MutableObjectiveCoefficient(object): +class _MutableObjectiveCoefficient(): def __init__(self, pyomo_var_id, var_map, expr, highs): self.expr = expr self.highs = highs @@ -113,7 +113,7 @@ def update(self): self.highs.changeColCost(col_ndx, value(self.expr)) -class _MutableObjectiveOffset(object): +class _MutableObjectiveOffset(): def __init__(self, expr, highs): self.expr = expr self.highs = highs @@ -122,7 +122,7 @@ def update(self): self.highs.changeObjectiveOffset(value(self.expr)) -class _MutableConstraintBounds(object): +class _MutableConstraintBounds(): def __init__(self, lower_expr, upper_expr, pyomo_con, con_map, highs): self.lower_expr = lower_expr self.upper_expr = upper_expr @@ -147,14 +147,14 @@ class Highs(PersistentBase, PersistentSolver): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = HighsConfig() - self._solver_options = dict() + self._solver_options = {} self._solver_model = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._mutable_helpers = dict() - self._mutable_bounds = dict() - self._objective_helpers = list() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_con_to_pyomo_con_map = {} + self._mutable_helpers = {} + self._mutable_bounds = {} + self._objective_helpers = [] self._last_results_object: Optional[HighsResults] = None self._sol = None @@ -301,10 +301,10 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - lbs = list() - ubs = list() - indices = list() - vtypes = list() + lbs = [] + ubs = [] + indices = [] + vtypes = [] current_num_vars = len(self._pyomo_var_to_solver_var_map) for v in variables: @@ -360,11 +360,11 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() current_num_cons = len(self._pyomo_con_to_solver_con_map) - lbs = list() - ubs = list() - starts = list() - var_indices = list() - coef_values = list() + lbs = [] + ubs = [] + starts = [] + var_indices = [] + coef_values = [] for con in cons: repn = generate_standard_repn( @@ -390,7 +390,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): highs=self._solver_model, ) if con not in self._mutable_helpers: - self._mutable_helpers[con] = list() + self._mutable_helpers[con] = [] self._mutable_helpers[con].append(mutable_linear_coefficient) if coef_val == 0: continue @@ -445,7 +445,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = list() + indices_to_remove = [] for con in cons: con_ndx = self._pyomo_con_to_solver_con_map.pop(con) del self._solver_con_to_pyomo_con_map[con_ndx] @@ -455,7 +455,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): len(indices_to_remove), np.array(indices_to_remove) ) con_ndx = 0 - new_con_map = dict() + new_con_map = {} for c in self._pyomo_con_to_solver_con_map.keys(): new_con_map[c] = con_ndx con_ndx += 1 @@ -474,7 +474,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = list() + indices_to_remove = [] for v in variables: v_id = id(v) v_ndx = self._pyomo_var_to_solver_var_map.pop(v_id) @@ -484,7 +484,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): len(indices_to_remove), np.array(indices_to_remove) ) v_ndx = 0 - new_var_map = dict() + new_var_map = {} for v_id in self._pyomo_var_to_solver_var_map.keys(): new_var_map[v_id] = v_ndx v_ndx += 1 @@ -497,10 +497,10 @@ def _update_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices = list() - lbs = list() - ubs = list() - vtypes = list() + indices = [] + lbs = [] + ubs = [] + vtypes = [] for v in variables: v_id = id(v) @@ -541,7 +541,7 @@ def _set_objective(self, obj): n = len(self._pyomo_var_to_solver_var_map) indices = np.arange(n) costs = np.zeros(n, dtype=np.double) - self._objective_helpers = list() + self._objective_helpers = [] if obj is None: sense = highspy.ObjSense.kMinimize self._solver_model.changeObjectiveOffset(0) @@ -692,7 +692,7 @@ def get_primals(self, vars_to_load=None, solution_number=0): res = ComponentMap() if vars_to_load is None: - var_ids_to_load = list() + var_ids_to_load = [] for v, ref_info in self._referenced_variables.items(): using_cons, using_sos, using_obj = ref_info if using_cons or using_sos or (using_obj is not None): @@ -737,7 +737,7 @@ def get_duals(self, cons_to_load=None): 'check the termination condition.' ) - res = dict() + res = {} if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) @@ -756,7 +756,7 @@ def get_slacks(self, cons_to_load=None): 'check the termination condition.' ) - res = dict() + res = {} if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 8c0716c6e1e..c03f6e145f3 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -45,7 +45,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(IpoptConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -129,10 +129,10 @@ def __init__( class Ipopt(PersistentSolver): def __init__(self, only_child_vars=False): self._config = IpoptConfig() - self._solver_options = dict() + self._solver_options = {} self._writer = NLWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = dict() + self._dual_sol = {} self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None @@ -347,7 +347,7 @@ def _parse_sol(self): + n_rc_lower ] - self._dual_sol = dict() + self._dual_sol = {} self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index de82b211092..2727cf2313b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,5 +1,5 @@ from pyomo.common.errors import PyomoException -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.appsi.base import TerminationCondition diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index 6b86deaa535..ce73b94ab74 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import pyomo.common.unittest as unittest +from pyomo.common import unittest from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.common.gsl import find_GSL diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index ec9f397bdc4..82db5f6286f 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,6 +1,6 @@ import pyomo.environ as pe from pyomo.common.dependencies import attempt_import -import pyomo.common.unittest as unittest +from pyomo.common import unittest parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized @@ -68,7 +68,7 @@ def _load_tests(solver_list, only_child_vars_list): - res = list() + res = [] for solver_name, solver in solver_list: for child_var_option in only_child_vars_list: test_name = f"{solver_name}_only_child_vars_{child_var_option}" @@ -979,8 +979,8 @@ def test_time_limit( m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) random.seed(0) - coefs = list() - lin_vars = list() + coefs = [] + lin_vars = [] for j in m.jobs: for t in m.tasks: coefs.append(random.uniform(0, 10)) diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py index 0d67ca4d01a..82a04b29e56 100644 --- a/pyomo/contrib/appsi/tests/test_base.py +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -37,16 +37,16 @@ def test_results(self): m.c1 = pe.Constraint(expr=m.x == 1) m.c2 = pe.Constraint(expr=m.y == 2) - primals = dict() + primals = {} primals[id(m.x)] = (m.x, 1) primals[id(m.y)] = (m.y, 2) - duals = dict() + duals = {} duals[m.c1] = 3 duals[m.c2] = 4 - rc = dict() + rc = {} rc[id(m.x)] = (m.x, 5) rc[id(m.y)] = (m.y, 6) - slacks = dict() + slacks = {} slacks[m.c1] = 7 slacks[m.c2] = 8 diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 7963cc31665..0924e3bbeed 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,5 +1,5 @@ from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available -import pyomo.common.unittest as unittest +from pyomo.common import unittest import math from pyomo.contrib.fbbt.tests.test_interval import IntervalTestBase @@ -7,7 +7,7 @@ @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestInterval(IntervalTestBase, unittest.TestCase): def setUp(self): - super(TestInterval, self).setUp() + super().setUp() self.add = cmodel.py_interval_add self.sub = cmodel.py_interval_sub self.mul = cmodel.py_interval_mul diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py index 9027080f08c..bfbbf5aecdf 100644 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -4,10 +4,10 @@ class _VarAndNamedExprCollector(ExpressionValueVisitor): def __init__(self): - self.named_expressions = dict() - self.variables = dict() - self.fixed_vars = dict() - self._external_functions = dict() + self.named_expressions = {} + self.variables = {} + self.fixed_vars = {} + self._external_functions = {} def visit(self, node, values): pass diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 7a7faadaabe..4376b9284fa 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,3 @@ -class WriterConfig(object): +class WriterConfig(): def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 8a76fa5f9eb..6a4a4ab2ff7 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -17,7 +17,7 @@ class LPWriter(PersistentBase): def __init__(self, only_child_vars=False): - super(LPWriter, self).__init__(only_child_vars=only_child_vars) + super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() @@ -25,11 +25,11 @@ def __init__(self, only_child_vars=False): self._con_labeler = None self._param_labeler = None self._obj_labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._pyomo_param_to_solver_param_map = dict() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_var_to_pyomo_var_map = {} + self._solver_con_to_pyomo_con_map = {} + self._pyomo_param_to_solver_param_map = {} self._expr_types = None @property @@ -89,7 +89,7 @@ def _add_params(self, params: List[_ParamData]): self._pyomo_param_to_solver_param_map[id(p)] = cp def _add_constraints(self, cons: List[_GeneralConstraintData]): - cmodel.process_lp_constraints(cons, self) + cmodel.process_lp_constraints() def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 9c739fd6ebb..d0bb443508d 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -20,18 +20,18 @@ class NLWriter(PersistentBase): def __init__(self, only_child_vars=False): - super(NLWriter, self).__init__(only_child_vars=only_child_vars) + super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() self._var_labeler = None self._con_labeler = None self._param_labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._pyomo_param_to_solver_param_map = dict() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_var_to_pyomo_var_map = {} + self._solver_con_to_pyomo_con_map = {} + self._pyomo_param_to_solver_param_map = {} self._expr_types = None @property @@ -172,8 +172,8 @@ def update_params(self): def _set_objective(self, obj: _GeneralObjectiveData): if obj is None: const = cmodel.Constant(0) - lin_vars = list() - lin_coef = list() + lin_vars = [] + lin_coef = [] nonlin = cmodel.Constant(0) sense = 0 else: @@ -240,7 +240,7 @@ def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = Non timer.stop('write file') def update(self, timer: HierarchicalTimer = None): - super(NLWriter, self).update(timer=timer) + super().update(timer=timer) self._set_pyomo_amplfunc_env() def get_ordered_vars(self): diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index 3b61a5901c3..297bc3d7617 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,4 +1,4 @@ -import pyomo.common.unittest as unittest +from pyomo.common import unittest from pyomo.common.tempfiles import TempfileManager import pyomo.environ as pe from pyomo.contrib import appsi From 561e5bc461e76ab973d05e5240b4e4c9194919cb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 15 Aug 2023 13:29:23 -0600 Subject: [PATCH 0009/1178] Revert accidental list/dict changes --- pyomo/contrib/appsi/base.py | 10 +++++----- pyomo/contrib/appsi/build.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 805e96dacf1..86268b04dbd 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -409,7 +409,7 @@ def get_duals( 'for the given problem type.' ) if cons_to_load is None: - duals = {self._duals} + duals = dict(self._duals) else: duals = {} for c in cons_to_load: @@ -426,7 +426,7 @@ def get_slacks( 'for the given problem type.' ) if cons_to_load is None: - slacks = {self._slacks} + slacks = dict(self._slacks) else: slacks = {} for c in cons_to_load: @@ -1116,7 +1116,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): v_id = id(v) if v_id not in self._referenced_variables: new_vars[v_id] = v - self.add_variables([new_vars.values()]) + self.add_variables(list(new_vars.values())) def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove = {} @@ -1125,7 +1125,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: vars_to_remove[v_id] = v - self.remove_variables([vars_to_remove.values()]) + self.remove_variables(list(vars_to_remove.values())) def add_constraints(self, cons: List[_GeneralConstraintData]): all_fixed_vars = {} @@ -1224,7 +1224,7 @@ def add_block(self, block): if p.mutable: for _p in p.values(): param_dict[id(_p)] = _p - self.add_params([param_dict.values()]) + self.add_params(list(param_dict.values())) if self._only_child_vars: self.add_variables( list( diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 6146272978c..37826cf85fb 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -63,8 +63,7 @@ def get_appsi_extension(in_setup=False, appsi_root=None): def build_appsi(args=[]): print('\n\n**** Building APPSI ****') - import setuptools - from distutils.dist import Distribution + from setuptools.dist import Distribution from pybind11.setup_helpers import build_ext import pybind11.setup_helpers from pyomo.common.envvar import PYOMO_CONFIG_DIR From 154518ea537944ab6aa976d00fc0544b0a929764 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 15 Aug 2023 14:01:43 -0600 Subject: [PATCH 0010/1178] Fix references to TerminationCondition.ok --- pyomo/contrib/appsi/base.py | 2 +- .../contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 10 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 +- pyomo/contrib/appsi/solvers/highs.py | 6 +- pyomo/contrib/appsi/solvers/ipopt.py | 10 ++-- .../solvers/tests/test_gurobi_persistent.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 55 +++++++++---------- 9 files changed, 47 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 86268b04dbd..d85e1ef9dfe 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -478,7 +478,7 @@ class Results(): >>> opt = appsi.solvers.Ipopt() >>> opt.config.load_solution = False >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.ok: #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP ... results.solution_loader.load_vars() #doctest:+SKIP ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 6d2cce76925..04092601c91 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -31,7 +31,7 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == appsi.base.TerminationCondition.ok + assert res.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 84a38ec3cdb..74b9aa8ba8e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -232,7 +232,7 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible @@ -307,7 +307,7 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition == TerminationCondition.ok + results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): @@ -316,7 +316,7 @@ def _parse_soln(self): results.best_feasible_objective = None else: results.best_feasible_objective = obj_val - elif results.termination_condition == TerminationCondition.ok: + elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -451,7 +451,7 @@ def get_duals(self, cons_to_load=None): if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -469,7 +469,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 47042586d0b..c459effe325 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -284,7 +284,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: @@ -336,7 +336,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'results.best_feasible_objective before loading a solution.' ) else: - if results.termination_condition != TerminationCondition.ok: + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 23a87e06f1c..2f79a8515a3 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -874,7 +874,7 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible elif status == grb.INF_OR_UNBD: @@ -925,7 +925,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if results.termination_condition != TerminationCondition.ok: + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 528fb2f3087..cd17f5d90e8 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -610,7 +610,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == highspy.HighsModelStatus.kInfeasible: results.termination_condition = TerminationCondition.infeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: @@ -633,7 +633,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if results.termination_condition == TerminationCondition.ok: + if results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, @@ -645,7 +645,7 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if results.termination_condition != TerminationCondition.ok: + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index c03f6e145f3..e19a68f6d85 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -303,7 +303,7 @@ def _parse_sol(self): termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif 'Problem may be infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible elif 'problem might be unbounded' in termination_line: @@ -384,7 +384,7 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition == TerminationCondition.ok + results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v, val in self._primal_sol.items(): @@ -395,7 +395,7 @@ def _parse_sol(self): results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif results.termination_condition == TerminationCondition.ok: + elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -526,7 +526,7 @@ def get_duals( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -544,7 +544,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 2727cf2313b..fcff8916b5b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -160,7 +160,7 @@ def test_lp(self): res = opt.solve(self.m) self.assertAlmostEqual(x + y, res.best_feasible_objective) self.assertAlmostEqual(x + y, res.best_objective_bound) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 82db5f6286f..9e7abf04e08 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -9,7 +9,6 @@ from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression -import os numpy, numpy_available = attempt_import('numpy') import random @@ -91,7 +90,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -99,7 +98,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) @@ -159,13 +158,13 @@ def test_range_constraint( m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -182,7 +181,7 @@ def test_reduced_costs( m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -200,13 +199,13 @@ def test_reduced_costs2( m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -236,7 +235,7 @@ def test_param_changes( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -274,7 +273,7 @@ def test_immutable_param( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -308,7 +307,7 @@ def test_equality( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -348,7 +347,7 @@ def test_linear_expression( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) @@ -378,7 +377,7 @@ def test_no_objective( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) @@ -407,7 +406,7 @@ def test_add_remove_cons( m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -418,7 +417,7 @@ def test_add_remove_cons( m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -430,7 +429,7 @@ def test_add_remove_cons( del m.c3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -456,7 +455,7 @@ def test_results_infeasible( res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual(res.termination_condition, TerminationCondition.ok) + self.assertNotEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) if opt_class is Ipopt: acceptable_termination_conditions = { TerminationCondition.infeasible, @@ -748,7 +747,7 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) @@ -785,7 +784,7 @@ def test_add_and_remove_vars( opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -799,7 +798,7 @@ def test_add_and_remove_vars( opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -808,7 +807,7 @@ def test_add_and_remove_vars( opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -869,7 +868,7 @@ def test_with_numpy( ) ) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -1211,14 +1210,14 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]) m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @@ -1241,7 +1240,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1251,7 +1250,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1273,12 +1272,12 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_va m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 3) From 7f0f73f5b7bd49a31a8bbcf101e4177bfeee03b4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 16 Aug 2023 09:41:54 -0600 Subject: [PATCH 0011/1178] SAVING STATE --- doc/OnlineDocs/conf.py | 7 ------- .../developer_reference/solvers.rst | 20 +++++++++++++++++++ pyomo/common/config.py | 2 +- pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 6 +++--- pyomo/contrib/appsi/solvers/ipopt.py | 6 +++--- .../solvers/tests/test_persistent_solvers.py | 2 +- 10 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 doc/OnlineDocs/developer_reference/solvers.rst diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 43df1263f82..d8939cf61dd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -146,13 +146,6 @@ html_theme = 'sphinx_rtd_theme' -# Force HTML4: If we don't explicitly force HTML4, then the background -# of the Parameters/Returns/Return type headers is shaded the same as the -# method prototype (tested 15 April 21 with Sphinx=3.5.4 and -# sphinx-rtd-theme=0.5.2). -html4_writer = True -# html5_writer = True - if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst new file mode 100644 index 00000000000..374ba4fbee8 --- /dev/null +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -0,0 +1,20 @@ +Solver Interfaces +================= + +Pyomo offers interfaces into multiple solvers, both commercial and open source. + + +Termination Conditions +---------------------- + +Pyomo offers a standard set of termination conditions to map to solver +returns. + +.. currentmodule:: pyomo.contrib.appsi.base + +.. autosummary:: + + TerminationCondition + + + diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 1b44d555b91..7bbcd693a72 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -2019,7 +2019,7 @@ def generate_documentation( ) if item_body is not None: deprecation_warning( - f"Overriding 'item_body' by passing strings to " + f"Overriding '{item_body}' by passing strings to " "generate_documentation is deprecated. Create an instance of a " "StringConfigFormatter and pass it as the 'format' argument.", version='6.6.0', diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index d85e1ef9dfe..5116b59322a 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -486,7 +486,7 @@ class Results(): ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP - ... elif results.termination_condition in {appsi.base.TerminationCondition.maxIterations, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP + ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP ... else: #doctest:+SKIP ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 74b9aa8ba8e..12e7555535e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -242,7 +242,7 @@ def _parse_soln(self): results.termination_condition = TerminationCondition.maxTimeLimit obj_val = float(termination_line.split()[-1]) elif termination_line.startswith('stopped on iterations'): - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit obj_val = float(termination_line.split()[-1]) else: results.termination_condition = TerminationCondition.unknown diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index c459effe325..08d6b11fc76 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -292,7 +292,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): elif status in [3, 103]: results.termination_condition = TerminationCondition.infeasible elif status in [10]: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status in [11, 25, 107, 131]: results.termination_condition = TerminationCondition.maxTimeLimit else: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 2f79a8515a3..dfe3b441cd8 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -884,9 +884,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == grb.CUTOFF: results.termination_condition = TerminationCondition.objectiveLimit elif status == grb.ITERATION_LIMIT: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status == grb.NODE_LIMIT: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status == grb.TIME_LIMIT: results.termination_condition = TerminationCondition.maxTimeLimit elif status == grb.SOLUTION_LIMIT: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index cd17f5d90e8..4ec4ebeffb1 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -612,7 +612,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kOptimal: results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == highspy.HighsModelStatus.kInfeasible: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == highspy.HighsModelStatus.kUnbounded: @@ -624,7 +624,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kTimeLimit: results.termination_condition = TerminationCondition.maxTimeLimit elif status == highspy.HighsModelStatus.kIterationLimit: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status == highspy.HighsModelStatus.kUnknown: results.termination_condition = TerminationCondition.unknown else: @@ -637,7 +637,7 @@ def _postsolve(self, timer: HierarchicalTimer): has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, - TerminationCondition.maxIterations, + TerminationCondition.iterationLimit, TerminationCondition.maxTimeLimit, }: if self._sol.value_valid: diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index e19a68f6d85..6580c9a004a 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -16,7 +16,7 @@ from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions -from typing import Optional, Sequence, NoReturn, List, Mapping +from typing import Optional, Sequence, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -305,11 +305,11 @@ def _parse_sol(self): if 'Optimal Solution Found' in termination_line: results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif 'Problem may be infeasible' in termination_line: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.locallyInfeasible elif 'problem might be unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif 'Maximum Number of Iterations Exceeded' in termination_line: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif 'Maximum CPU Time Exceeded' in termination_line: results.termination_condition = TerminationCondition.maxTimeLimit else: diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 9e7abf04e08..23236827a11 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1014,7 +1014,7 @@ def test_time_limit( if type(opt) is Cbc: # I can't figure out why CBC is reporting max iter... self.assertIn( res.termination_condition, - {TerminationCondition.maxIterations, TerminationCondition.maxTimeLimit}, + {TerminationCondition.iterationLimit, TerminationCondition.maxTimeLimit}, ) else: self.assertEqual( From cda74ae09f83345a78ac513e0db42dd55e7f97a5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 17 Aug 2023 08:53:57 -0600 Subject: [PATCH 0012/1178] Begin documentation for sovlers --- doc/OnlineDocs/developer_reference/index.rst | 1 + doc/OnlineDocs/developer_reference/solvers.rst | 7 +++---- pyomo/common/config.py | 2 +- pyomo/contrib/appsi/solvers/ipopt.py | 5 ++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/index.rst b/doc/OnlineDocs/developer_reference/index.rst index 8c29150015c..0f0f636abee 100644 --- a/doc/OnlineDocs/developer_reference/index.rst +++ b/doc/OnlineDocs/developer_reference/index.rst @@ -12,3 +12,4 @@ scripts using Pyomo. config.rst deprecation.rst expressions/index.rst + solvers.rst diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 374ba4fbee8..d48e270cc7c 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -10,11 +10,10 @@ Termination Conditions Pyomo offers a standard set of termination conditions to map to solver returns. -.. currentmodule:: pyomo.contrib.appsi.base +.. currentmodule:: pyomo.contrib.appsi -.. autosummary:: - - TerminationCondition +.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition + :noindex: diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 7bbcd693a72..61e4f682a2a 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -2019,7 +2019,7 @@ def generate_documentation( ) if item_body is not None: deprecation_warning( - f"Overriding '{item_body}' by passing strings to " + "Overriding 'item_body' by passing strings to " "generate_documentation is deprecated. Create an instance of a " "StringConfigFormatter and pass it as the 'format' argument.", version='6.6.0', diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 6580c9a004a..68dcdae2492 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -297,9 +297,8 @@ def _parse_sol(self): solve_cons = self._writer.get_ordered_cons() results = Results() - f = open(self._filename + '.sol', 'r') - all_lines = list(f.readlines()) - f.close() + with open(self._filename + '.sol', 'r') as f: + all_lines = list(f.readlines()) termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: From bb6bf5e67b31820760325f2f5f3e9c6c83094d3f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 25 Aug 2023 13:58:03 -0400 Subject: [PATCH 0013/1178] add highs support --- pyomo/contrib/mindtpy/config_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index ed0c86baae9..713ab539660 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -538,7 +538,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', TODO: feasibility pump now fails with appsi_highs #2951 + 'appsi_highs' ] ), description='MIP subsolver name', @@ -620,7 +620,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', + 'appsi_highs', ] ), description='MIP subsolver for regularization problem', From da8a56e837fb43dec321af088fd498c980429db5 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 25 Aug 2023 14:03:07 -0400 Subject: [PATCH 0014/1178] change test mip solver to highs --- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index e872eccc670..4efd9493b8a 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -56,7 +56,7 @@ QCP_model._generate_model() extreme_model_list = [LP_model.model, QCP_model.model] -required_solvers = ('ipopt', 'glpk') +required_solvers = ('ipopt', 'appsi_highs') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index b5bfbe62553..95516af11fd 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -12,7 +12,7 @@ from pyomo.environ import SolverFactory, value from pyomo.opt import TerminationCondition -required_solvers = ('ipopt', 'glpk') +required_solvers = ('ipopt', 'appsi_highs') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index 697a63d17c8..18b7a420674 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -17,8 +17,7 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'cplex') -# TODO: 'appsi_highs' will fail here. +required_solvers = ('ipopt', 'appsi_highs') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: From 95f4cc2a281fdf235800ae9d82bc27c11d6f1f18 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 25 Aug 2023 14:13:24 -0400 Subject: [PATCH 0015/1178] black format --- pyomo/contrib/mindtpy/config_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index 713ab539660..2769d336e31 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -538,7 +538,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - 'appsi_highs' + 'appsi_highs', ] ), description='MIP subsolver name', From b0f6747208fe453bc459f22c58035fc5169b2cd8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 10:17:50 -0600 Subject: [PATCH 0016/1178] Fix termination conditions --- .../contrib/appsi/solvers/tests/test_persistent_solvers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 23236827a11..df2eccd5eef 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -458,12 +458,14 @@ def test_results_infeasible( self.assertNotEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) if opt_class is Ipopt: acceptable_termination_conditions = { - TerminationCondition.infeasible, + TerminationCondition.provenInfeasible, + TerminationCondition.locallyInfeasible, TerminationCondition.unbounded, } else: acceptable_termination_conditions = { - TerminationCondition.infeasible, + TerminationCondition.provenInfeasible, + TerminationCondition.locallyInfeasible, TerminationCondition.infeasibleOrUnbounded, } self.assertIn(res.termination_condition, acceptable_termination_conditions) From 2b7d62f18c091aa264b0ee8b2e48301a85c86a18 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 10:22:46 -0600 Subject: [PATCH 0017/1178] Modify termination conditions for solvers --- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 12e7555535e..9a4f098d08b 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -235,7 +235,7 @@ def _parse_soln(self): results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif 'unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif termination_line.startswith('stopped on time'): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 08d6b11fc76..9c7683b81cf 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -290,7 +290,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): elif status in [4, 119, 134]: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status in [3, 103]: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif status in [10]: results.termination_condition = TerminationCondition.iterationLimit elif status in [11, 25, 107, 131]: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index dfe3b441cd8..339c001369e 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -876,7 +876,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == grb.OPTIMAL: # optimal results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == grb.INFEASIBLE: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif status == grb.INF_OR_UNBD: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == grb.UNBOUNDED: From 3b94b79df27f20fbd5d7276e2e4e60d538ef3344 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 10:53:07 -0600 Subject: [PATCH 0018/1178] Update params for Solver base class --- pyomo/contrib/appsi/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 5116b59322a..f50a1e6135f 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -20,7 +20,7 @@ from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs from pyomo.common.timing import HierarchicalTimer -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory @@ -177,6 +177,8 @@ class InterfaceConfig(ConfigDict): report_timing: bool - wrapper If True, then some timing information will be printed at the end of the solve. + threads: integer - sent to solver + Number of threads to be used by a solver. """ def __init__( @@ -199,6 +201,7 @@ def __init__( self.declare('load_solution', ConfigValue(domain=bool)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) + self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) @@ -744,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, tee=False, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, tee: bool = False, timer: HierarchicalTimer = None, **kwargs ) -> Results: """ Solve a Pyomo model. From 49f226d247b30bf8071c091171075dce0a7153c5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 11:13:07 -0600 Subject: [PATCH 0019/1178] Change stream_solver back to tee --- pyomo/contrib/appsi/base.py | 16 +++++++--------- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 2 +- pyomo/contrib/appsi/solvers/ipopt.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 2 +- 7 files changed, 14 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index f50a1e6135f..62c87c5a0c9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -165,7 +165,7 @@ class InterfaceConfig(ConfigDict): ---------- time_limit: float - sent to solver Time limit for the solver - stream_solver: bool - wrapper + tee: bool - wrapper If True, then the solver log goes to stdout load_solution: bool - wrapper If False, then the values of the primal variables will not be @@ -197,7 +197,7 @@ def __init__( visibility=visibility, ) - self.declare('stream_solver', ConfigValue(domain=bool)) + self.declare('tee', ConfigValue(domain=bool)) self.declare('load_solution', ConfigValue(domain=bool)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) @@ -206,7 +206,7 @@ def __init__( self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) - self.stream_solver: bool = False + self.tee: bool = False self.load_solution: bool = True self.symbolic_solver_labels: bool = False self.report_timing: bool = False @@ -454,7 +454,7 @@ def get_reduced_costs( return rc -class Results(): +class Results: """ Attributes ---------- @@ -747,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, tee: bool = False, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs ) -> Results: """ Solve a Pyomo model. @@ -756,8 +756,6 @@ def solve( ---------- model: _BlockData The Pyomo model to be solved - tee: bool - Show solver output in the terminal timer: HierarchicalTimer An option timer for reporting timing **kwargs @@ -1635,7 +1633,7 @@ def update(self, timer: HierarchicalTimer = None): } -class LegacySolverInterface(): +class LegacySolverInterface: def solve( self, model: _BlockData, @@ -1653,7 +1651,7 @@ def solve( ): original_config = self.config self.config = self.config() - self.config.stream_solver = tee + self.config.tee = tee self.config.load_solution = load_solutions self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 9a4f098d08b..35071ab17ea 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -383,7 +383,7 @@ def _check_and_escape_options(): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 9c7683b81cf..7f9844fc21d 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -245,7 +245,7 @@ def _apply_solver(self, timer: HierarchicalTimer): log_stream = LogStream( level=self.config.log_level, logger=self.config.solver_output_logger ) - if config.stream_solver: + if config.tee: def _process_stream(arg): sys.stdout.write(arg) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 339c001369e..3f8eab638b0 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -353,7 +353,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -1384,7 +1384,7 @@ def set_callback(self, func=None): >>> _c = _add_cut(4) # this is an arbitrary choice >>> >>> opt = appsi.solvers.Gurobi() - >>> opt.config.stream_solver = True + >>> opt.config.tee = True >>> opt.set_instance(m) # doctest:+SKIP >>> opt.gurobi_options['PreCrush'] = 1 >>> opt.gurobi_options['LazyConstraints'] = 1 diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 4ec4ebeffb1..e5c43d27c8d 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -211,7 +211,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 68dcdae2492..da42fc0be41 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -430,7 +430,7 @@ def _apply_solver(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) cmd = [ diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index df2eccd5eef..2d579611761 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -368,7 +368,7 @@ def test_no_objective( m.b2 = pe.Param(mutable=True) m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) - opt.config.stream_solver = True + opt.config.tee = True params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: From 50f64526a7187061c696433bd09c6765bfbbd822 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 11:26:06 -0600 Subject: [PATCH 0020/1178] Isolate tests to just APPSI for speed --- .github/workflows/test_branches.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 99d5f7fc1a8..d1d6f73870d 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -598,8 +598,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo `pwd`/pyomo-model-libraries \ - `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" + pyomo/contrib/appsi --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 From 2e3ad3ad20389c8d656855f3bf27074276174b99 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:09:02 -0600 Subject: [PATCH 0021/1178] Remove kwargs for now --- pyomo/contrib/appsi/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 62c87c5a0c9..5ce5421ee86 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -747,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, timer: HierarchicalTimer = None, ) -> Results: """ Solve a Pyomo model. From 0a0a67da17eb25c76761e59d45b5ed5c9b432ec2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:21:28 -0600 Subject: [PATCH 0022/1178] Per Michael Bynum, remove cbc and cplex tests as C++ lp_writer won't be sustained --- .../solvers/tests/test_persistent_solvers.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 2d579611761..135f36d3695 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -6,7 +6,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression @@ -21,14 +21,12 @@ all_solvers = [ ('gurobi', Gurobi), ('ipopt', Ipopt), - ('cplex', Cplex), - ('cbc', Cbc), ('highs', Highs), ] -mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] +mip_solvers = [('gurobi', Gurobi), ('highs', Highs)] nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] -miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] +miqcqp_solvers = [('gurobi', Gurobi)] only_child_vars_options = [True, False] @@ -1013,15 +1011,9 @@ def test_time_limit( opt.config.time_limit = 0 opt.config.load_solution = False res = opt.solve(m) - if type(opt) is Cbc: # I can't figure out why CBC is reporting max iter... - self.assertIn( - res.termination_condition, - {TerminationCondition.iterationLimit, TerminationCondition.maxTimeLimit}, - ) - else: - self.assertEqual( - res.termination_condition, TerminationCondition.maxTimeLimit - ) + self.assertEqual( + res.termination_condition, TerminationCondition.maxTimeLimit + ) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( From af5ee141afa06d3c7fafdffd8b01d362c604c826 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:27:00 -0600 Subject: [PATCH 0023/1178] Turn off test_examples; uses cplex --- pyomo/contrib/appsi/examples/tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index ffcecaf0c5f..db6e2910b77 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -5,7 +5,7 @@ from pyomo.contrib import appsi -@unittest.skipUnless(cmodel_available, 'appsi extensions are not available') +@unittest.skip('Currently turning off cplex support') class TestExamples(unittest.TestCase): def test_getting_started(self): try: From 3028138884b3b9125ffe6bebfc38058a8759c70c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:39:33 -0600 Subject: [PATCH 0024/1178] Allow macOS IPOPT download; update ubuntu download; try using ipopt instead of cplex for getting_started --- pyomo/contrib/appsi/examples/getting_started.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 04092601c91..d907283f663 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -16,7 +16,7 @@ def main(plot=True, n_points=200): m.c1 = pe.Constraint(expr=m.y >= (m.x + 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= (m.x - m.p) ** 2) - opt = appsi.solvers.Cplex() # create an APPSI solver interface + opt = appsi.solvers.Ipopt() # create an APPSI solver interface opt.config.load_solution = False # modify the config options # change how automatic updates are handled opt.update_config.check_for_new_or_removed_vars = False From ad7d9e028f9fdd68f7057fb4f45aea78dc0ebf75 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:43:44 -0600 Subject: [PATCH 0025/1178] Allow macOS IPOPT download; update ubuntu download; try using ipopt instead of cplex for getting_started --- .github/workflows/test_branches.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d1d6f73870d..9640171a7c1 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -390,10 +390,10 @@ jobs: IPOPT_TAR=${DOWNLOAD_DIR}/ipopt.tar.gz if test ! -e $IPOPT_TAR; then echo "...downloading Ipopt" - if test "${{matrix.TARGET}}" == osx; then - echo "IDAES Ipopt not available on OSX" - exit 0 - fi + # if test "${{matrix.TARGET}}" == osx; then + # echo "IDAES Ipopt not available on OSX" + # exit 0 + # fi URL=https://github.com/IDAES/idaes-ext RELEASE=$(curl --max-time 150 --retry 8 \ -L -s -H 'Accept: application/json' ${URL}/releases/latest) @@ -401,7 +401,11 @@ jobs: URL=${URL}/releases/download/$VER if test "${{matrix.TARGET}}" == linux; then curl --max-time 150 --retry 8 \ - -L $URL/idaes-solvers-ubuntu2004-x86_64.tar.gz \ + -L $URL/idaes-solvers-ubuntu2204-x86_64.tar.gz \ + > $IPOPT_TAR + elseif test "${{matrix.TARGET}}" == osx; then + curl --max-time 150 --retry 8 \ + -L $URL/idaes-solvers-darwin-x86_64.tar.gz \ > $IPOPT_TAR else curl --max-time 150 --retry 8 \ From cbe8b9902070df5c7ac0809bf4ae7965bb400299 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:56:46 -0600 Subject: [PATCH 0026/1178] Fix broken bash --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9640171a7c1..ed0f9350206 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -403,7 +403,7 @@ jobs: curl --max-time 150 --retry 8 \ -L $URL/idaes-solvers-ubuntu2204-x86_64.tar.gz \ > $IPOPT_TAR - elseif test "${{matrix.TARGET}}" == osx; then + elif test "${{matrix.TARGET}}" == osx; then curl --max-time 150 --retry 8 \ -L $URL/idaes-solvers-darwin-x86_64.tar.gz \ > $IPOPT_TAR From 2d55e658b6b1a9effe5983dae21af75bcce319ed Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 13:06:22 -0600 Subject: [PATCH 0027/1178] Change untar command --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index ed0f9350206..76bdcfd0587 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -414,7 +414,7 @@ jobs: fi fi cd $IPOPT_DIR - tar -xzi < $IPOPT_TAR + tar -xzf < $IPOPT_TAR echo "" echo "$IPOPT_DIR" ls -l $IPOPT_DIR From 630662942f30c2fd70bca7e1392c532057e2ab8c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 13:11:14 -0600 Subject: [PATCH 0028/1178] Trying a different untar command --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 76bdcfd0587..0ac37747a65 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -414,7 +414,7 @@ jobs: fi fi cd $IPOPT_DIR - tar -xzf < $IPOPT_TAR + tar -xz < $IPOPT_TAR echo "" echo "$IPOPT_DIR" ls -l $IPOPT_DIR From 0e1c8c750981f76828d5cb3283b1468c9f439541 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 13:16:41 -0600 Subject: [PATCH 0029/1178] Turning on examples test --- pyomo/contrib/appsi/examples/tests/test_examples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index db6e2910b77..2ea089a8cc6 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -5,14 +5,14 @@ from pyomo.contrib import appsi -@unittest.skip('Currently turning off cplex support') +@unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestExamples(unittest.TestCase): def test_getting_started(self): try: import numpy as np except: raise unittest.SkipTest('numpy is not available') - opt = appsi.solvers.Cplex() + opt = appsi.solvers.Ipopt() if not opt.available(): - raise unittest.SkipTest('cplex is not available') + raise unittest.SkipTest('ipopt is not available') getting_started.main(plot=False, n_points=10) From d2b91264275b5611d7b577dc91d2b6c38e5b7db1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 14:37:13 -0600 Subject: [PATCH 0030/1178] Change test_examples skipping --- pyomo/contrib/appsi/examples/tests/test_examples.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index 2ea089a8cc6..7c577366c41 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,17 +1,16 @@ from pyomo.contrib.appsi.examples import getting_started from pyomo.common import unittest -import pyomo.environ as pe +from pyomo.common.dependencies import attempt_import from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib import appsi +numpy, numpy_available = attempt_import('numpy') + @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') +@unittest.skipUnless(numpy_available, 'numpy is not available') class TestExamples(unittest.TestCase): def test_getting_started(self): - try: - import numpy as np - except: - raise unittest.SkipTest('numpy is not available') opt = appsi.solvers.Ipopt() if not opt.available(): raise unittest.SkipTest('ipopt is not available') From d0cb12542307dbe0dae2320853c1054a552e9dd1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 16:38:28 -0600 Subject: [PATCH 0031/1178] SAVE POINT: starting to move items out of appsi --- pyomo/contrib/appsi/base.py | 31 +- pyomo/solver/__init__.py | 14 + pyomo/solver/base.py | 1240 +++++++++++++++++++++++++++ pyomo/solver/config.py | 270 ++++++ pyomo/solver/solution.py | 256 ++++++ pyomo/solver/tests/test_base.py | 0 pyomo/solver/tests/test_config.py | 0 pyomo/solver/tests/test_solution.py | 0 pyomo/solver/tests/test_util.py | 0 pyomo/solver/util.py | 23 + 10 files changed, 1830 insertions(+), 4 deletions(-) create mode 100644 pyomo/solver/__init__.py create mode 100644 pyomo/solver/base.py create mode 100644 pyomo/solver/config.py create mode 100644 pyomo/solver/solution.py create mode 100644 pyomo/solver/tests/test_base.py create mode 100644 pyomo/solver/tests/test_config.py create mode 100644 pyomo/solver/tests/test_solution.py create mode 100644 pyomo/solver/tests/test_util.py create mode 100644 pyomo/solver/util.py diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 5ce5421ee86..aa17489c4d5 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -165,7 +165,7 @@ class InterfaceConfig(ConfigDict): ---------- time_limit: float - sent to solver Time limit for the solver - tee: bool - wrapper + tee: bool If True, then the solver log goes to stdout load_solution: bool - wrapper If False, then the values of the primal variables will not be @@ -720,7 +720,7 @@ def __init__( # End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). -class Solver(abc.ABC): +class SolverBase(abc.ABC): class Availability(enum.IntEnum): NotFound = 0 BadVersion = -1 @@ -747,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, + self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs ) -> Results: """ Solve a Pyomo model. @@ -827,8 +827,31 @@ def is_persistent(self): """ return False +# In a non-persistent interface, when the solver dies, it'll return +# everthing it is going to return. And when you parse, you'll parse everything, +# whether or not you needed it. -class PersistentSolver(Solver): +# In a persistent interface, if all I really care about is to keep going +# until the objective gets better. I may not need to parse the dual or state +# vars. If I only need the objective, why waste time bringing that extra +# cruft back? Why not just return what you ask for when you ask for it? + +# All the `gets_` is to be able to retrieve from the solver. Because the +# persistent interface is still holding onto the solver's definition, +# it saves time. Also helps avoid assuming that you are loading a model. + +# There is an argument whether or not the get methods could be called load. + +# For non-persistent, there are also questions about how we load everything. +# We tend to just load everything because it might disappear otherwise. +# In the file interface, we tend to parse everything, and the option is to turn +# it all off. We still parse everything... + +# IDEAL SITUATION -- +# load_solutions = True -> straight into model; otherwise, into results object + + +class PersistentSolver(SolverBase): def is_persistent(self): return True diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py new file mode 100644 index 00000000000..64c6452d06d --- /dev/null +++ b/pyomo/solver/__init__.py @@ -0,0 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from . import util +from . import base +from . import solution diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py new file mode 100644 index 00000000000..b6d9e1592cb --- /dev/null +++ b/pyomo/solver/base.py @@ -0,0 +1,1240 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import abc +import enum +from typing import ( + Sequence, + Dict, + Optional, + Mapping, + NoReturn, + List, + Tuple, +) +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.block import _BlockData +from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from .utils.get_objective import get_objective +from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.errors import ApplicationError +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +import os +from pyomo.opt.results.results_ import SolverResults as LegacySolverResults +from pyomo.opt.results.solution import ( + Solution as LegacySolution, + SolutionStatus as LegacySolutionStatus, +) +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) +from pyomo.core.kernel.objective import minimize +from pyomo.core.base import SymbolMap +from .cmodel import cmodel, cmodel_available +from pyomo.core.staleflag import StaleFlagManager +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.solver import ( + SolutionLoader, + SolutionLoaderBase, + UpdateConfig +) + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + unknown = 42 + + """The solver exited because the convergence criteria were satisfied""" + convergenceCriteriaSatisfied = 0 + + """The solver exited due to a time limit""" + maxTimeLimit = 1 + + """The solver exited due to an iteration limit""" + iterationLimit = 2 + + """The solver exited due to an objective limit""" + objectiveLimit = 3 + + """The solver exited due to a minimum step length""" + minStepLength = 4 + + """The solver exited because the problem is unbounded""" + unbounded = 5 + + """The solver exited because the problem is proven infeasible""" + provenInfeasible = 6 + + """The solver exited because the problem was found to be locally infeasible""" + locallyInfeasible = 7 + + """The solver exited because the problem is either infeasible or unbounded""" + infeasibleOrUnbounded = 8 + + """The solver exited due to an error""" + error = 9 + + """The solver exited because it was interrupted""" + interrupted = 10 + + """The solver exited due to licensing problems""" + licensingProblems = 11 + + +class SolutionStatus(enum.IntEnum): + """ + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + For now, we are choosing to use IntEnum such that return values are numerically + assigned in increasing order. + """ + + """No (single) solution found; possible that a population of solutions was returned""" + noSolution = 0 + + """Solution point does not satisfy some domains and/or constraints""" + infeasible = 10 + + """Feasible solution identified""" + feasible = 20 + + """Optimal solution identified""" + optimal = 30 + + +class Results: + """ + Attributes + ---------- + termination_condition: TerminationCondition + The reason the solver exited. This is a member of the + TerminationCondition enum. + best_feasible_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + best_objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + + Here is an example workflow: + + >>> import pyomo.environ as pe + >>> from pyomo.contrib import appsi + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var() + >>> m.obj = pe.Objective(expr=m.x**2) + >>> opt = appsi.solvers.Ipopt() + >>> opt.config.load_solution = False + >>> results = opt.solve(m) #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP + ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars() #doctest:+SKIP + ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP + ... elif results.best_feasible_objective is not None: #doctest:+SKIP + ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP + ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP + ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP + ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP + ... else: #doctest:+SKIP + ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP + """ + + def __init__(self): + self.solution_loader: SolutionLoaderBase = SolutionLoader( + None, None, None, None + ) + self.termination_condition: TerminationCondition = TerminationCondition.unknown + self.best_feasible_objective: Optional[float] = None + self.best_objective_bound: Optional[float] = None + + def __str__(self): + s = '' + s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' + s += 'best_objective_bound: ' + str(self.best_objective_bound) + return s + + +class SolverBase(abc.ABC): + class Availability(enum.IntEnum): + NotFound = 0 + BadVersion = -1 + BadLicense = -2 + FullLicense = 1 + LimitedLicense = 2 + NeedsCompiledExtension = -3 + + def __bool__(self): + return self._value_ > 0 + + def __format__(self, format_spec): + # We want general formatting of this Enum to return the + # formatted string value and not the int (which is the + # default implementation from IntEnum) + return format(self.name, format_spec) + + def __str__(self): + # Note: Python 3.11 changed the core enums so that the + # "mixin" type for standard enums overrides the behavior + # specified in __format__. We will override str() here to + # preserve the previous behavior + return self.name + + @abc.abstractmethod + def solve( + self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs + ) -> Results: + """ + Solve a Pyomo model. + + Parameters + ---------- + model: _BlockData + The Pyomo model to be solved + timer: HierarchicalTimer + An option timer for reporting timing + **kwargs + Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) + + Returns + ------- + results: Results + A results object + """ + pass + + @abc.abstractmethod + def available(self): + """Test if the solver is available on this system. + + Nominally, this will return True if the solver interface is + valid and can be used to solve problems and False if it cannot. + + Note that for licensed solvers there are a number of "levels" of + available: depending on the license, the solver may be available + with limitations on problem size or runtime (e.g., 'demo' + vs. 'community' vs. 'full'). In these cases, the solver may + return a subclass of enum.IntEnum, with members that resolve to + True if the solver is available (possibly with limitations). + The Enum may also have multiple members that all resolve to + False indicating the reason why the interface is not available + (not found, bad license, unsupported version, etc). + + Returns + ------- + available: Solver.Availability + An enum that indicates "how available" the solver is. + Note that the enum can be cast to bool, which will + be True if the solver is runable at all and False + otherwise. + """ + pass + + @abc.abstractmethod + def version(self) -> Tuple: + """ + Returns + ------- + version: tuple + A tuple representing the version + """ + + @property + @abc.abstractmethod + def config(self): + """ + An object for configuring solve options. + + Returns + ------- + InterfaceConfig + An object for configuring pyomo solve options such as the time limit. + These options are mostly independent of the solver. + """ + pass + + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ + return False + + +class PersistentSolver(SolverBase): + def is_persistent(self): + return True + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slack values + """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) + + @property + @abc.abstractmethod + def update_config(self) -> UpdateConfig: + pass + + @abc.abstractmethod + def set_instance(self, model): + pass + + @abc.abstractmethod + def add_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def add_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def add_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def remove_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def remove_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def remove_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + pass + + @abc.abstractmethod + def update_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def update_params(self): + pass + + + +""" +What can change in a pyomo model? +- variables added or removed +- constraints added or removed +- objective changed +- objective expr changed +- params added or removed +- variable modified + - lb + - ub + - fixed or unfixed + - domain + - value +- constraint modified + - lower + - upper + - body + - active or not +- named expressions modified + - expr +- param modified + - value + +Ideas: +- Consider explicitly handling deactivated constraints; favor deactivation over removal + and activation over addition + +Notes: +- variable bounds cannot be updated with mutable params; you must call update_variables +""" + + +class PersistentBase(abc.ABC): + def __init__(self, only_child_vars=False): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._update_config = UpdateConfig() + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + self.use_extensions = False + self._only_child_vars = only_child_vars + + @property + def update_config(self): + return self._update_config + + @update_config.setter + def update_config(self, val: UpdateConfig): + self._update_config = val + + def set_instance(self, model): + saved_update_config = self.update_config + self.__init__() + self.update_config = saved_update_config + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(con.body, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + if self._only_child_vars: + self.add_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects(Var, descend_into=True) + ).values() + ) + ) + self.add_constraints( + list(block.component_data_objects(Constraint, descend_into=True, active=True)) + ) + self.add_sos_constraints( + list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) + ) + self.remove_sos_constraints( + list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) + ) + if self._only_child_vars: + self.remove_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects( + ctype=Var, descend_into=True + ) + ).values() + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.update_config + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if self._only_child_vars and ( + config.check_for_new_or_removed_vars or config.update_vars + ): + current_vars_dict = { + id(v): v + for v in self._model.component_data_objects(Var, descend_into=True) + } + for v_id, v in current_vars_dict.items(): + if v_id not in self._vars: + new_vars.append(v) + for v_id, v_tuple in self._vars.items(): + if v_id not in current_vars_dict: + old_vars.append(v_tuple[0]) + elif config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if self._only_child_vars and config.update_vars: + vars_to_check = [] + for v_id, v in current_vars_dict.items(): + if v_id not in new_vars_set: + vars_to_check.append(v) + elif config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif (fixed is not v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.update_config.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.update_config.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.update_config.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') + + +# Everything below here preserves backwards compatibility + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + TerminationCondition.unknown: LegacySolutionStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.minStepLength: LegacySolutionStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, + TerminationCondition.unbounded: LegacySolutionStatus.unbounded, + TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, + TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, + TerminationCondition.error: LegacySolutionStatus.error, + TerminationCondition.interrupted: LegacySolutionStatus.error, + TerminationCondition.licensingProblems: LegacySolutionStatus.error, +} + + +class LegacySolverInterface: + def solve( + self, + model: _BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + ): + original_config = self.config + self.config = self.config() + self.config.tee = tee + self.config.load_solution = load_solutions + self.config.symbolic_solver_labels = symbolic_solver_labels + self.config.time_limit = timelimit + self.config.report_timing = report_timing + if solver_io is not None: + raise NotImplementedError('Still working on this') + if suffixes is not None: + raise NotImplementedError('Still working on this') + if logfile is not None: + raise NotImplementedError('Still working on this') + if 'keepfiles' in self.config: + self.config.keepfiles = keepfiles + if solnfile is not None: + if 'filename' in self.config: + filename = os.path.splitext(solnfile)[0] + self.config.filename = filename + original_options = self.options + if options is not None: + self.options = options + + results: Results = super().solve(model) + + legacy_results = LegacySolverResults() + legacy_soln = LegacySolution() + legacy_results.solver.status = legacy_solver_status_map[ + results.termination_condition + ] + legacy_results.solver.termination_condition = legacy_termination_condition_map[ + results.termination_condition + ] + legacy_soln.status = legacy_solution_status_map[results.termination_condition] + legacy_results.solver.termination_message = str(results.termination_condition) + + obj = get_objective(model) + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.best_objective_bound + legacy_results.problem.upper_bound = results.best_feasible_objective + else: + legacy_results.problem.upper_bound = results.best_objective_bound + legacy_results.problem.lower_bound = results.best_feasible_objective + if ( + results.best_feasible_objective is not None + and results.best_objective_bound is not None + ): + legacy_soln.gap = abs( + results.best_feasible_objective - results.best_objective_bound + ) + else: + legacy_soln.gap = None + + symbol_map = SymbolMap() + symbol_map.byObject = dict(self.symbol_map.byObject) + symbol_map.bySymbol = dict(self.symbol_map.bySymbol) + symbol_map.aliases = dict(self.symbol_map.aliases) + symbol_map.default_labeler = self.symbol_map.default_labeler + model.solutions.add_symbol_map(symbol_map) + legacy_results._smap_id = id(symbol_map) + + delete_legacy_soln = True + if load_solutions: + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + model.dual[c] = val + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + model.slack[c] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + model.rc[v] = val + elif results.best_feasible_objective is not None: + delete_legacy_soln = False + for v, val in results.solution_loader.get_primals().items(): + legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + symbol = symbol_map.getSymbol(c) + if symbol in legacy_soln.constraint: + legacy_soln.constraint[symbol]['Slack'] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + legacy_soln.variable['Rc'] = val + + legacy_results.solution.insert(legacy_soln) + if delete_legacy_soln: + legacy_results.solution.delete(0) + + self.config = original_config + self.options = original_options + + return legacy_results + + def available(self, exception_flag=True): + ans = super().available() + if exception_flag and not ans: + raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') + return bool(ans) + + def license_is_valid(self) -> bool: + """Test if the solver license is valid on this system. + + Note that this method is included for compatibility with the + legacy SolverFactory interface. Unlicensed or open source + solvers will return True by definition. Licensed solvers will + return True if a valid license is found. + + Returns + ------- + available: bool + True if the solver license is valid. Otherwise, False. + + """ + return bool(self.available()) + + @property + def options(self): + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + return getattr(self, solver_name + '_options') + raise NotImplementedError('Could not find the correct options') + + @options.setter + def options(self, val): + found = False + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + setattr(self, solver_name + '_options', val) + found = True + if not found: + raise NotImplementedError('Could not find the correct options') + + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + pass + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverInterface, cls): + pass + + LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py new file mode 100644 index 00000000000..ab9c30a0549 --- /dev/null +++ b/pyomo/solver/config.py @@ -0,0 +1,270 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from typing import Optional +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt + + +class InterfaceConfig(ConfigDict): + """ + Attributes + ---------- + time_limit: float - sent to solver + Time limit for the solver + tee: bool + If True, then the solver log goes to stdout + load_solution: bool - wrapper + If False, then the values of the primal variables will not be + loaded into the model + symbolic_solver_labels: bool - sent to solver + If True, the names given to the solver will reflect the names + of the pyomo components. Cannot be changed after set_instance + is called. + report_timing: bool - wrapper + If True, then some timing information will be printed at the + end of the solve. + threads: integer - sent to solver + Number of threads to be used by a solver. + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('tee', ConfigValue(domain=bool)) + self.declare('load_solution', ConfigValue(domain=bool)) + self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) + self.declare('report_timing', ConfigValue(domain=bool)) + self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) + + self.time_limit: Optional[float] = self.declare( + 'time_limit', ConfigValue(domain=NonNegativeFloat) + ) + self.tee: bool = False + self.load_solution: bool = True + self.symbolic_solver_labels: bool = False + self.report_timing: bool = False + + +class MIPInterfaceConfig(InterfaceConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) + self.declare('relax_integrality', ConfigValue(domain=bool)) + + self.mip_gap: Optional[float] = None + self.relax_integrality: bool = False + + +class UpdateConfig(ConfigDict): + """ + This is necessary for persistent solvers. + + Attributes + ---------- + check_for_new_or_removed_constraints: bool + check_for_new_or_removed_vars: bool + check_for_new_or_removed_params: bool + update_constraints: bool + update_vars: bool + update_params: bool + update_named_expressions: bool + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + if doc is None: + doc = 'Configuration options to detect changes in model between solves' + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare( + 'check_for_new_or_removed_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old constraints will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_constraints() + and opt.remove_constraints() or when you are certain constraints are not being + added to/removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old variables will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_variables() and + opt.remove_variables() or when you are certain variables are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old parameters will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_params() and + opt.remove_params() or when you are certain parameters are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old objectives will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.set_objective() or + when you are certain objectives are not being added to / removed from the model.""", + ), + ) + self.declare( + 'update_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing constraints will not be automatically detected on + subsequent solves. This includes changes to the lower, body, and upper attributes of + constraints. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain constraints + are not being modified.""", + ), + ) + self.declare( + 'update_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing variables will not be automatically detected on + subsequent solves. This includes changes to the lb, ub, domain, and fixed + attributes of variables. Use False only when manually updating the solver with + opt.update_variables() or when you are certain variables are not being modified.""", + ), + ) + self.declare( + 'update_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to parameter values will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.update_params() or when you are certain parameters are not being modified.""", + ), + ) + self.declare( + 'update_named_expressions', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to Expressions will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain + Expressions are not being modified.""", + ), + ) + self.declare( + 'update_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to objectives will not be automatically detected on + subsequent solves. This includes the expr and sense attributes of objectives. Use + False only when manually updating the solver with opt.set_objective() or when you are + certain objectives are not being modified.""", + ), + ) + self.declare( + 'treat_fixed_vars_as_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + This is an advanced option that should only be used in special circumstances. + With the default setting of True, fixed variables will be treated like parameters. + This means that z == x*y will be linear if x or y is fixed and the constraint + can be written to an LP file. If the value of the fixed variable gets changed, we have + to completely reprocess all constraints using that variable. If + treat_fixed_vars_as_params is False, then constraints will be processed as if fixed + variables are not fixed, and the solver will be told the variable is fixed. This means + z == x*y could not be written to an LP file even if x and/or y is fixed. However, + updating the values of fixed variables is much faster this way.""", + ), + ) + + self.check_for_new_or_removed_constraints: bool = True + self.check_for_new_or_removed_vars: bool = True + self.check_for_new_or_removed_params: bool = True + self.check_for_new_objective: bool = True + self.update_constraints: bool = True + self.update_vars: bool = True + self.update_params: bool = True + self.update_named_expressions: bool = True + self.update_objective: bool = True + self.treat_fixed_vars_as_params: bool = True diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py new file mode 100644 index 00000000000..2d422736f2c --- /dev/null +++ b/pyomo/solver/solution.py @@ -0,0 +1,256 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import abc +from typing import ( + Sequence, + Dict, + Optional, + Mapping, + MutableMapping, + NoReturn, +) + +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData +from pyomo.common.collections import ComponentMap +from pyomo.core.staleflag import StaleFlagManager + + +class SolutionLoaderBase(abc.ABC): + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to var value. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution value should be retrieved. If vars_to_load is None, + then the values for all variables will be retrieved. + + Returns + ------- + primals: ComponentMap + Maps variables to solution values + """ + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to dual value. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all + constraints will be retrieved. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError(f'{type(self)} does not support the get_duals method') + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to slack. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slacks + """ + raise NotImplementedError( + f'{type(self)} does not support the get_slacks method' + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to reduced cost. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the + reduced costs for all variables will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variables to reduced costs + """ + raise NotImplementedError( + f'{type(self)} does not support the get_reduced_costs method' + ) + + +class SolutionLoader(SolutionLoaderBase): + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + slacks: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + slacks: dict + maps Constraint to slack value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._slacks = slacks + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = {} + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._slacks is None: + raise RuntimeError( + 'Solution loader does not currently have valid slacks. Please ' + 'check the termination condition and ensure the solver returns slacks ' + 'for the given problem type.' + ) + if cons_to_load is None: + slacks = dict(self._slacks) + else: + slacks = {} + for c in cons_to_load: + slacks[c] = self._slacks[c] + return slacks + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver): + self._solver = solver + self._valid = True + + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_duals(cons_to_load=cons_to_load) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_slacks(cons_to_load=cons_to_load) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + self._assert_solution_still_valid() + return self._solver.get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False + + + + diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/tests/test_util.py b/pyomo/solver/tests/test_util.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py new file mode 100644 index 00000000000..8c768061678 --- /dev/null +++ b/pyomo/solver/util.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +class SolverUtils: + pass + +class SubprocessSolverUtils: + pass + +class DirectSolverUtils: + pass + +class PersistentSolverUtils: + pass + From 434ff9d984ee5446c150fbcf8af82df6716b4f30 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 08:59:25 -0600 Subject: [PATCH 0032/1178] Separate base class from APPSI --- pyomo/contrib/appsi/__init__.py | 1 - pyomo/contrib/appsi/base.py | 1836 ----------------- .../contrib/appsi/examples/getting_started.py | 3 +- pyomo/contrib/appsi/fbbt.py | 5 +- pyomo/contrib/appsi/solvers/cbc.py | 23 +- pyomo/contrib/appsi/solvers/cplex.py | 24 +- pyomo/contrib/appsi/solvers/gurobi.py | 25 +- pyomo/contrib/appsi/solvers/highs.py | 14 +- pyomo/contrib/appsi/solvers/ipopt.py | 24 +- .../solvers/tests/test_gurobi_persistent.py | 5 +- .../solvers/tests/test_persistent_solvers.py | 2 +- pyomo/contrib/appsi/tests/test_base.py | 91 - pyomo/contrib/appsi/writers/lp_writer.py | 6 +- pyomo/contrib/appsi/writers/nl_writer.py | 12 +- pyomo/environ/__init__.py | 1 + pyomo/solver/__init__.py | 4 +- pyomo/solver/base.py | 70 +- pyomo/solver/tests/test_base.py | 91 + 18 files changed, 159 insertions(+), 2078 deletions(-) delete mode 100644 pyomo/contrib/appsi/base.py delete mode 100644 pyomo/contrib/appsi/tests/test_base.py diff --git a/pyomo/contrib/appsi/__init__.py b/pyomo/contrib/appsi/__init__.py index df3ba212448..0134a96f363 100644 --- a/pyomo/contrib/appsi/__init__.py +++ b/pyomo/contrib/appsi/__init__.py @@ -1,4 +1,3 @@ -from . import base from . import solvers from . import writers from . import fbbt diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py deleted file mode 100644 index aa17489c4d5..00000000000 --- a/pyomo/contrib/appsi/base.py +++ /dev/null @@ -1,1836 +0,0 @@ -import abc -import enum -from typing import ( - Sequence, - Dict, - Optional, - Mapping, - NoReturn, - List, - Tuple, - MutableMapping, -) -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var -from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.block import _BlockData -from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.collections import ComponentMap -from .utils.get_objective import get_objective -from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs -from pyomo.common.timing import HierarchicalTimer -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt -from pyomo.common.errors import ApplicationError -from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory -import os -from pyomo.opt.results.results_ import SolverResults as LegacySolverResults -from pyomo.opt.results.solution import ( - Solution as LegacySolution, - SolutionStatus as LegacySolutionStatus, -) -from pyomo.opt.results.solver import ( - TerminationCondition as LegacyTerminationCondition, - SolverStatus as LegacySolverStatus, -) -from pyomo.core.kernel.objective import minimize -from pyomo.core.base import SymbolMap -from .cmodel import cmodel, cmodel_available -from pyomo.core.staleflag import StaleFlagManager -from pyomo.core.expr.numvalue import NumericConstant - - -# # TerminationCondition - -# We currently have: Termination condition, solver status, and solution status. -# LL: Michael was trying to go for simplicity. All three conditions can be confusing. -# It is likely okay to have termination condition and solver status. - -# ## Open Questions (User Perspective) -# - Did I (the user) get a reasonable answer back from the solver? -# - If the answer is not reasonable, can I figure out why? - -# ## Our Goal -# Solvers normally tell you what they did and hope the users understand that. -# *We* want to try to return that information but also _help_ the user. - -# ## Proposals -# PROPOSAL 1: PyomoCondition and SolverCondition -# - SolverCondition: what the solver said -# - PyomoCondition: what we interpret that the solver said - -# PROPOSAL 2: TerminationCondition contains... -# - Some finite list of conditions -# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? -# - Replace `optimal` with `normal` or `ok` for the termination flag; `optimal` can be used differently for the solver flag -# - You can use something else like `local`, `global`, `feasible` for solution status - - -class TerminationCondition(enum.Enum): - """ - An enumeration for checking the termination condition of solvers - """ - - """unknown serves as both a default value, and it is used when no other enum member makes sense""" - unknown = 42 - - """The solver exited because the convergence criteria were satisfied""" - convergenceCriteriaSatisfied = 0 - - """The solver exited due to a time limit""" - maxTimeLimit = 1 - - """The solver exited due to an iteration limit""" - iterationLimit = 2 - - """The solver exited due to an objective limit""" - objectiveLimit = 3 - - """The solver exited due to a minimum step length""" - minStepLength = 4 - - """The solver exited because the problem is unbounded""" - unbounded = 5 - - """The solver exited because the problem is proven infeasible""" - provenInfeasible = 6 - - """The solver exited because the problem was found to be locally infeasible""" - locallyInfeasible = 7 - - """The solver exited because the problem is either infeasible or unbounded""" - infeasibleOrUnbounded = 8 - - """The solver exited due to an error""" - error = 9 - - """The solver exited because it was interrupted""" - interrupted = 10 - - """The solver exited due to licensing problems""" - licensingProblems = 11 - - -class SolutionStatus(enum.IntEnum): - """ - An enumeration for interpreting the result of a termination. This describes the designated - status by the solver to be loaded back into the model. - - For now, we are choosing to use IntEnum such that return values are numerically - assigned in increasing order. - """ - - """No (single) solution found; possible that a population of solutions was returned""" - noSolution = 0 - - """Solution point does not satisfy some domains and/or constraints""" - infeasible = 10 - - """Feasible solution identified""" - feasible = 20 - - """Optimal solution identified""" - optimal = 30 - - -# # InterfaceConfig - -# The idea here (currently / in theory) is that a call to solve will have a keyword argument `solver_config`: -# ``` -# solve(model, solver_config=...) -# config = self.config(solver_config) -# ``` - -# We have several flavors of options: -# - Solver options -# - Standardized options -# - Wrapper options -# - Interface options -# - potentially... more? - -# ## The Options - -# There are three basic structures: flat, doubly-nested, separate dicts. -# We need to pick between these three structures (and stick with it). - -# **Flat: Clear interface; ambiguous about what goes where; better solve interface.** <- WINNER -# Doubly: More obscure interface; less ambiguity; better programmatic interface. -# SepDicts: Clear delineation; **kwargs becomes confusing (what maps to what?) (NOT HAPPENING) - - -class InterfaceConfig(ConfigDict): - """ - Attributes - ---------- - time_limit: float - sent to solver - Time limit for the solver - tee: bool - If True, then the solver log goes to stdout - load_solution: bool - wrapper - If False, then the values of the primal variables will not be - loaded into the model - symbolic_solver_labels: bool - sent to solver - If True, the names given to the solver will reflect the names - of the pyomo components. Cannot be changed after set_instance - is called. - report_timing: bool - wrapper - If True, then some timing information will be printed at the - end of the solve. - threads: integer - sent to solver - Number of threads to be used by a solver. - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare('tee', ConfigValue(domain=bool)) - self.declare('load_solution', ConfigValue(domain=bool)) - self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) - self.declare('report_timing', ConfigValue(domain=bool)) - self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) - - self.time_limit: Optional[float] = self.declare( - 'time_limit', ConfigValue(domain=NonNegativeFloat) - ) - self.tee: bool = False - self.load_solution: bool = True - self.symbolic_solver_labels: bool = False - self.report_timing: bool = False - - -class MIPInterfaceConfig(InterfaceConfig): - """ - Attributes - ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) - self.declare('relax_integrality', ConfigValue(domain=bool)) - - self.mip_gap: Optional[float] = None - self.relax_integrality: bool = False - - -# # SolutionLoaderBase - -# This is an attempt to answer the issue of persistent/non-persistent solution -# loading. This is an attribute of the results object (not the solver). - -# You wouldn't ask the solver to load a solution into a model. You would -# ask the result to load the solution - into the model you solved. -# The results object points to relevant elements; elements do NOT point to -# the results object. - -# Per Michael: This may be a bit clunky; but it works. -# Per Siirola: We may want to rethink `load_vars` and `get_primals`. In particular, -# this is for efficiency - don't create a dictionary you don't need to. And what is -# the client use-case for `get_primals`? - - -class SolutionLoaderBase(abc.ABC): - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: - """ - Load the solution of the primal variables into the value attribute of the variables. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. - """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): - v.set_value(val, skip_validation=True) - StaleFlagManager.mark_all_as_stale(delayed=True) - - @abc.abstractmethod - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Returns a ComponentMap mapping variable to var value. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution value should be retrieved. If vars_to_load is None, - then the values for all variables will be retrieved. - - Returns - ------- - primals: ComponentMap - Maps variables to solution values - """ - pass - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Returns a dictionary mapping constraint to dual value. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all - constraints will be retrieved. - - Returns - ------- - duals: dict - Maps constraints to dual values - """ - raise NotImplementedError(f'{type(self)} does not support the get_duals method') - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Returns a dictionary mapping constraint to slack. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slacks - """ - raise NotImplementedError( - f'{type(self)} does not support the get_slacks method' - ) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Returns a ComponentMap mapping variable to reduced cost. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the - reduced costs for all variables will be loaded. - - Returns - ------- - reduced_costs: ComponentMap - Maps variables to reduced costs - """ - raise NotImplementedError( - f'{type(self)} does not support the get_reduced_costs method' - ) - - -class SolutionLoader(SolutionLoaderBase): - def __init__( - self, - primals: Optional[MutableMapping], - duals: Optional[MutableMapping], - slacks: Optional[MutableMapping], - reduced_costs: Optional[MutableMapping], - ): - """ - Parameters - ---------- - primals: dict - maps id(Var) to (var, value) - duals: dict - maps Constraint to dual value - slacks: dict - maps Constraint to slack value - reduced_costs: dict - maps id(Var) to (var, reduced_cost) - """ - self._primals = primals - self._duals = duals - self._slacks = slacks - self._reduced_costs = reduced_costs - - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - if self._primals is None: - raise RuntimeError( - 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' - ) - if vars_to_load is None: - return ComponentMap(self._primals.values()) - else: - primals = ComponentMap() - for v in vars_to_load: - primals[v] = self._primals[id(v)][1] - return primals - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - if self._duals is None: - raise RuntimeError( - 'Solution loader does not currently have valid duals. Please ' - 'check the termination condition and ensure the solver returns duals ' - 'for the given problem type.' - ) - if cons_to_load is None: - duals = dict(self._duals) - else: - duals = {} - for c in cons_to_load: - duals[c] = self._duals[c] - return duals - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - if self._slacks is None: - raise RuntimeError( - 'Solution loader does not currently have valid slacks. Please ' - 'check the termination condition and ensure the solver returns slacks ' - 'for the given problem type.' - ) - if cons_to_load is None: - slacks = dict(self._slacks) - else: - slacks = {} - for c in cons_to_load: - slacks[c] = self._slacks[c] - return slacks - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - if self._reduced_costs is None: - raise RuntimeError( - 'Solution loader does not currently have valid reduced costs. Please ' - 'check the termination condition and ensure the solver returns reduced ' - 'costs for the given problem type.' - ) - if vars_to_load is None: - rc = ComponentMap(self._reduced_costs.values()) - else: - rc = ComponentMap() - for v in vars_to_load: - rc[v] = self._reduced_costs[id(v)][1] - return rc - - -class Results: - """ - Attributes - ---------- - termination_condition: TerminationCondition - The reason the solver exited. This is a member of the - TerminationCondition enum. - best_feasible_objective: float - If a feasible solution was found, this is the objective value of - the best solution found. If no feasible solution was found, this is - None. - best_objective_bound: float - The best objective bound found. For minimization problems, this is - the lower bound. For maximization problems, this is the upper bound. - For solvers that do not provide an objective bound, this should be -inf - (minimization) or inf (maximization) - - Here is an example workflow: - - >>> import pyomo.environ as pe - >>> from pyomo.contrib import appsi - >>> m = pe.ConcreteModel() - >>> m.x = pe.Var() - >>> m.obj = pe.Objective(expr=m.x**2) - >>> opt = appsi.solvers.Ipopt() - >>> opt.config.load_solution = False - >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP - ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars() #doctest:+SKIP - ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP - ... elif results.best_feasible_objective is not None: #doctest:+SKIP - ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP - ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP - ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP - ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP - ... else: #doctest:+SKIP - ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP - """ - - def __init__(self): - self.solution_loader: SolutionLoaderBase = SolutionLoader( - None, None, None, None - ) - self.termination_condition: TerminationCondition = TerminationCondition.unknown - self.best_feasible_objective: Optional[float] = None - self.best_objective_bound: Optional[float] = None - - def __str__(self): - s = '' - s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' - s += 'best_objective_bound: ' + str(self.best_objective_bound) - return s - - -class UpdateConfig(ConfigDict): - """ - Attributes - ---------- - check_for_new_or_removed_constraints: bool - check_for_new_or_removed_vars: bool - check_for_new_or_removed_params: bool - update_constraints: bool - update_vars: bool - update_params: bool - update_named_expressions: bool - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - if doc is None: - doc = 'Configuration options to detect changes in model between solves' - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare( - 'check_for_new_or_removed_constraints', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old constraints will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_constraints() - and opt.remove_constraints() or when you are certain constraints are not being - added to/removed from the model.""", - ), - ) - self.declare( - 'check_for_new_or_removed_vars', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old variables will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_variables() and - opt.remove_variables() or when you are certain variables are not being added to / - removed from the model.""", - ), - ) - self.declare( - 'check_for_new_or_removed_params', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old parameters will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_params() and - opt.remove_params() or when you are certain parameters are not being added to / - removed from the model.""", - ), - ) - self.declare( - 'check_for_new_objective', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old objectives will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.set_objective() or - when you are certain objectives are not being added to / removed from the model.""", - ), - ) - self.declare( - 'update_constraints', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to existing constraints will not be automatically detected on - subsequent solves. This includes changes to the lower, body, and upper attributes of - constraints. Use False only when manually updating the solver with - opt.remove_constraints() and opt.add_constraints() or when you are certain constraints - are not being modified.""", - ), - ) - self.declare( - 'update_vars', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to existing variables will not be automatically detected on - subsequent solves. This includes changes to the lb, ub, domain, and fixed - attributes of variables. Use False only when manually updating the solver with - opt.update_variables() or when you are certain variables are not being modified.""", - ), - ) - self.declare( - 'update_params', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to parameter values will not be automatically detected on - subsequent solves. Use False only when manually updating the solver with - opt.update_params() or when you are certain parameters are not being modified.""", - ), - ) - self.declare( - 'update_named_expressions', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to Expressions will not be automatically detected on - subsequent solves. Use False only when manually updating the solver with - opt.remove_constraints() and opt.add_constraints() or when you are certain - Expressions are not being modified.""", - ), - ) - self.declare( - 'update_objective', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to objectives will not be automatically detected on - subsequent solves. This includes the expr and sense attributes of objectives. Use - False only when manually updating the solver with opt.set_objective() or when you are - certain objectives are not being modified.""", - ), - ) - self.declare( - 'treat_fixed_vars_as_params', - ConfigValue( - domain=bool, - default=True, - doc=""" - This is an advanced option that should only be used in special circumstances. - With the default setting of True, fixed variables will be treated like parameters. - This means that z == x*y will be linear if x or y is fixed and the constraint - can be written to an LP file. If the value of the fixed variable gets changed, we have - to completely reprocess all constraints using that variable. If - treat_fixed_vars_as_params is False, then constraints will be processed as if fixed - variables are not fixed, and the solver will be told the variable is fixed. This means - z == x*y could not be written to an LP file even if x and/or y is fixed. However, - updating the values of fixed variables is much faster this way.""", - ), - ) - - self.check_for_new_or_removed_constraints: bool = True - self.check_for_new_or_removed_vars: bool = True - self.check_for_new_or_removed_params: bool = True - self.check_for_new_objective: bool = True - self.update_constraints: bool = True - self.update_vars: bool = True - self.update_params: bool = True - self.update_named_expressions: bool = True - self.update_objective: bool = True - self.treat_fixed_vars_as_params: bool = True - - -# # Solver - -# ## Open Question: What does 'solve' look like? - -# We may want to use the 80/20 rule here - we support 80% of the cases; anything -# fancier than that is going to require "writing code." The 80% would be offerings -# that are supported as part of the `pyomo` script. - -# ## Configs - -# We will likely have two configs for `solve`: standardized config (processes `**kwargs`) -# and implicit ConfigDict with some specialized options. - -# These have to be separated because there is a set that need to be passed -# directly to the solver. The other is Pyomo options / our standardized options -# (a few of which might be passed directly to solver, e.g., time_limit). - -# ## Contained Methods - -# We do not like `symbol_map`; it's keyed towards file-based interfaces. That -# is the `lp` writer; the `nl` writer doesn't need that (and in fact, it's -# obnoxious). The new `nl` writer returns back more meaningful things to the `nl` -# interface. - -# If the writer needs a symbol map, it will return it. But it is _not_ a -# solver thing. So it does not need to continue to exist in the solver interface. - -# All other options are reasonable. - -# ## Other (maybe should be contained) Methods - -# There are other methods in other solvers such as `warmstart`, `sos`; do we -# want to continue to support and/or offer those features? - -# The solver interface is not responsible for telling the client what -# it can do, e.g., `supports_sos2`. This is actually a contract between -# the solver and its writer. - -# End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). - - -class SolverBase(abc.ABC): - class Availability(enum.IntEnum): - NotFound = 0 - BadVersion = -1 - BadLicense = -2 - FullLicense = 1 - LimitedLicense = 2 - NeedsCompiledExtension = -3 - - def __bool__(self): - return self._value_ > 0 - - def __format__(self, format_spec): - # We want general formatting of this Enum to return the - # formatted string value and not the int (which is the - # default implementation from IntEnum) - return format(self.name, format_spec) - - def __str__(self): - # Note: Python 3.11 changed the core enums so that the - # "mixin" type for standard enums overrides the behavior - # specified in __format__. We will override str() here to - # preserve the previous behavior - return self.name - - @abc.abstractmethod - def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs - ) -> Results: - """ - Solve a Pyomo model. - - Parameters - ---------- - model: _BlockData - The Pyomo model to be solved - timer: HierarchicalTimer - An option timer for reporting timing - **kwargs - Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) - - Returns - ------- - results: Results - A results object - """ - pass - - @abc.abstractmethod - def available(self): - """Test if the solver is available on this system. - - Nominally, this will return True if the solver interface is - valid and can be used to solve problems and False if it cannot. - - Note that for licensed solvers there are a number of "levels" of - available: depending on the license, the solver may be available - with limitations on problem size or runtime (e.g., 'demo' - vs. 'community' vs. 'full'). In these cases, the solver may - return a subclass of enum.IntEnum, with members that resolve to - True if the solver is available (possibly with limitations). - The Enum may also have multiple members that all resolve to - False indicating the reason why the interface is not available - (not found, bad license, unsupported version, etc). - - Returns - ------- - available: Solver.Availability - An enum that indicates "how available" the solver is. - Note that the enum can be cast to bool, which will - be True if the solver is runable at all and False - otherwise. - """ - pass - - @abc.abstractmethod - def version(self) -> Tuple: - """ - Returns - ------- - version: tuple - A tuple representing the version - """ - - @property - @abc.abstractmethod - def config(self): - """ - An object for configuring solve options. - - Returns - ------- - InterfaceConfig - An object for configuring pyomo solve options such as the time limit. - These options are mostly independent of the solver. - """ - pass - - def is_persistent(self): - """ - Returns - ------- - is_persistent: bool - True if the solver is a persistent solver. - """ - return False - -# In a non-persistent interface, when the solver dies, it'll return -# everthing it is going to return. And when you parse, you'll parse everything, -# whether or not you needed it. - -# In a persistent interface, if all I really care about is to keep going -# until the objective gets better. I may not need to parse the dual or state -# vars. If I only need the objective, why waste time bringing that extra -# cruft back? Why not just return what you ask for when you ask for it? - -# All the `gets_` is to be able to retrieve from the solver. Because the -# persistent interface is still holding onto the solver's definition, -# it saves time. Also helps avoid assuming that you are loading a model. - -# There is an argument whether or not the get methods could be called load. - -# For non-persistent, there are also questions about how we load everything. -# We tend to just load everything because it might disappear otherwise. -# In the file interface, we tend to parse everything, and the option is to turn -# it all off. We still parse everything... - -# IDEAL SITUATION -- -# load_solutions = True -> straight into model; otherwise, into results object - - -class PersistentSolver(SolverBase): - def is_persistent(self): - return True - - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: - """ - Load the solution of the primal variables into the value attribute of the variables. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. - """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): - v.set_value(val, skip_validation=True) - StaleFlagManager.mark_all_as_stale(delayed=True) - - @abc.abstractmethod - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - pass - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Declare sign convention in docstring here. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - duals: dict - Maps constraints to dual values - """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Parameters - ---------- - cons_to_load: list - A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slack values - """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Parameters - ---------- - vars_to_load: list - A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs - will be loaded. - - Returns - ------- - reduced_costs: ComponentMap - Maps variable to reduced cost - """ - raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) - ) - - @property - @abc.abstractmethod - def update_config(self) -> UpdateConfig: - pass - - @abc.abstractmethod - def set_instance(self, model): - pass - - @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def add_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def add_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def remove_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): - pass - - @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def update_params(self): - pass - - -class PersistentSolutionLoader(SolutionLoaderBase): - def __init__(self, solver: PersistentSolver): - self._solver = solver - self._valid = True - - def _assert_solution_still_valid(self): - if not self._valid: - raise RuntimeError('The results in the solver are no longer valid.') - - def get_primals(self, vars_to_load=None): - self._assert_solution_still_valid() - return self._solver.get_primals(vars_to_load=vars_to_load) - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver.get_duals(cons_to_load=cons_to_load) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver.get_slacks(cons_to_load=cons_to_load) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - self._assert_solution_still_valid() - return self._solver.get_reduced_costs(vars_to_load=vars_to_load) - - def invalidate(self): - self._valid = False - - -""" -What can change in a pyomo model? -- variables added or removed -- constraints added or removed -- objective changed -- objective expr changed -- params added or removed -- variable modified - - lb - - ub - - fixed or unfixed - - domain - - value -- constraint modified - - lower - - upper - - body - - active or not -- named expressions modified - - expr -- param modified - - value - -Ideas: -- Consider explicitly handling deactivated constraints; favor deactivation over removal - and activation over addition - -Notes: -- variable bounds cannot be updated with mutable params; you must call update_variables -""" - - -class PersistentBase(abc.ABC): - def __init__(self, only_child_vars=False): - self._model = None - self._active_constraints = {} # maps constraint to (lower, body, upper) - self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) - self._params = {} # maps param id to param - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._named_expressions = ( - {} - ) # maps constraint to list of tuples (named_expr, named_expr.expr) - self._external_functions = ComponentMap() - self._obj_named_expressions = [] - self._update_config = UpdateConfig() - self._referenced_variables = ( - {} - ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = {} - self._vars_referenced_by_obj = [] - self._expr_types = None - self.use_extensions = False - self._only_child_vars = only_child_vars - - @property - def update_config(self): - return self._update_config - - @update_config.setter - def update_config(self, val: UpdateConfig): - self._update_config = val - - def set_instance(self, model): - saved_update_config = self.update_config - self.__init__() - self.update_config = saved_update_config - self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() - self.add_block(model) - if self._objective is None: - self.set_objective(None) - - @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): - pass - - def add_variables(self, variables: List[_GeneralVarData]): - for v in variables: - if id(v) in self._referenced_variables: - raise ValueError( - 'variable {name} has already been added'.format(name=v.name) - ) - self._referenced_variables[id(v)] = [{}, {}, None] - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._add_variables(variables) - - @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): - pass - - def add_params(self, params: List[_ParamData]): - for p in params: - self._params[id(p)] = p - self._add_params(params) - - @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = {} - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - new_vars[v_id] = v - self.add_variables(list(new_vars.values())) - - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = {} - for v in variables: - v_id = id(v) - ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] - if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: - vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) - - def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = {} - for con in cons: - if con in self._named_expressions: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = (con.lower, con.body, con.upper) - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(con.body, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(con.body) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [(e, e.expr) for e in named_exprs] - if len(external_functions) > 0: - self._external_functions[con] = external_functions - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][0][con] = None - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - all_fixed_vars[id(v)] = v - self._add_constraints(cons) - for v in all_fixed_vars.values(): - v.fix() - - @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def add_sos_constraints(self, cons: List[_SOSConstraintData]): - for con in cons: - if con in self._vars_referenced_by_con: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = tuple() - variables = con.get_variables() - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [] - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][1][con] = None - self._add_sos_constraints(cons) - - @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): - pass - - def set_objective(self, obj: _GeneralObjectiveData): - if self._objective is not None: - for v in self._vars_referenced_by_obj: - self._referenced_variables[id(v)][2] = None - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_obj) - self._external_functions.pop(self._objective, None) - if obj is not None: - self._objective = obj - self._objective_expr = obj.expr - self._objective_sense = obj.sense - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(obj.expr) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._obj_named_expressions = [(i, i.expr) for i in named_exprs] - if len(external_functions) > 0: - self._external_functions[obj] = external_functions - self._vars_referenced_by_obj = variables - for v in variables: - self._referenced_variables[id(v)][2] = obj - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - self._set_objective(obj) - for v in fixed_vars: - v.fix() - else: - self._vars_referenced_by_obj = [] - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._obj_named_expressions = [] - self._set_objective(obj) - - def add_block(self, block): - param_dict = {} - for p in block.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) - if self._only_child_vars: - self.add_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects(Var, descend_into=True) - ).values() - ) - ) - self.add_constraints( - list(block.component_data_objects(Constraint, descend_into=True, active=True)) - ) - self.add_sos_constraints( - list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) - ) - obj = get_objective(block) - if obj is not None: - self.set_objective(obj) - - @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def remove_constraints(self, cons: List[_GeneralConstraintData]): - self._remove_constraints(cons) - for con in cons: - if con not in self._named_expressions: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][0].pop(con) - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - self._external_functions.pop(con, None) - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): - self._remove_sos_constraints(cons) - for con in cons: - if con not in self._vars_referenced_by_con: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][1].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): - pass - - def remove_variables(self, variables: List[_GeneralVarData]): - self._remove_variables(variables) - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - raise ValueError( - 'cannot remove variable {name} - it has not been added'.format( - name=v.name - ) - ) - cons_using, sos_using, obj_using = self._referenced_variables[v_id] - if cons_using or sos_using or (obj_using is not None): - raise ValueError( - 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( - name=v.name - ) - ) - del self._referenced_variables[v_id] - del self._vars[v_id] - - @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): - pass - - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) - for p in params: - del self._params[id(p)] - - def remove_block(self, block): - self.remove_constraints( - list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) - ) - self.remove_sos_constraints( - list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) - ) - if self._only_child_vars: - self.remove_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects( - ctype=Var, descend_into=True - ) - ).values() - ) - ) - self.remove_params( - list( - dict( - (id(p), p) - for p in block.component_data_objects( - ctype=Param, descend_into=True - ) - ).values() - ) - ) - - @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): - pass - - def update_variables(self, variables: List[_GeneralVarData]): - for v in variables: - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._update_variables(variables) - - @abc.abstractmethod - def update_params(self): - pass - - def update(self, timer: HierarchicalTimer = None): - if timer is None: - timer = HierarchicalTimer() - config = self.update_config - new_vars = [] - old_vars = [] - new_params = [] - old_params = [] - new_cons = [] - old_cons = [] - old_sos = [] - new_sos = [] - current_vars_dict = {} - current_cons_dict = {} - current_sos_dict = {} - timer.start('vars') - if self._only_child_vars and ( - config.check_for_new_or_removed_vars or config.update_vars - ): - current_vars_dict = { - id(v): v - for v in self._model.component_data_objects(Var, descend_into=True) - } - for v_id, v in current_vars_dict.items(): - if v_id not in self._vars: - new_vars.append(v) - for v_id, v_tuple in self._vars.items(): - if v_id not in current_vars_dict: - old_vars.append(v_tuple[0]) - elif config.update_vars: - start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - timer.stop('vars') - timer.start('params') - if config.check_for_new_or_removed_params: - current_params_dict = {} - for p in self._model.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - current_params_dict[id(_p)] = _p - for p_id, p in current_params_dict.items(): - if p_id not in self._params: - new_params.append(p) - for p_id, p in self._params.items(): - if p_id not in current_params_dict: - old_params.append(p) - timer.stop('params') - timer.start('cons') - if config.check_for_new_or_removed_constraints or config.update_constraints: - current_cons_dict = { - c: None - for c in self._model.component_data_objects( - Constraint, descend_into=True, active=True - ) - } - current_sos_dict = { - c: None - for c in self._model.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - } - for c in current_cons_dict.keys(): - if c not in self._vars_referenced_by_con: - new_cons.append(c) - for c in current_sos_dict.keys(): - if c not in self._vars_referenced_by_con: - new_sos.append(c) - for c in self._vars_referenced_by_con.keys(): - if c not in current_cons_dict and c not in current_sos_dict: - if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) - ): - old_cons.append(c) - else: - assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) - ) - old_sos.append(c) - self.remove_constraints(old_cons) - self.remove_sos_constraints(old_sos) - timer.stop('cons') - timer.start('params') - self.remove_params(old_params) - - # sticking this between removal and addition - # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() - - self.add_params(new_params) - timer.stop('params') - timer.start('vars') - self.add_variables(new_vars) - timer.stop('vars') - timer.start('cons') - self.add_constraints(new_cons) - self.add_sos_constraints(new_sos) - new_cons_set = set(new_cons) - new_sos_set = set(new_sos) - new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = {} - need_to_set_objective = False - if config.update_constraints: - cons_to_update = [] - sos_to_update = [] - for c in current_cons_dict.keys(): - if c not in new_cons_set: - cons_to_update.append(c) - for c in current_sos_dict.keys(): - if c not in new_sos_set: - sos_to_update.append(c) - for c in cons_to_update: - lower, body, upper = self._active_constraints[c] - new_lower, new_body, new_upper = c.lower, c.body, c.upper - if new_body is not body: - cons_to_remove_and_add[c] = None - continue - if new_lower is not lower: - if ( - type(new_lower) is NumericConstant - and type(lower) is NumericConstant - and new_lower.value == lower.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - if new_upper is not upper: - if ( - type(new_upper) is NumericConstant - and type(upper) is NumericConstant - and new_upper.value == upper.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - self.remove_sos_constraints(sos_to_update) - self.add_sos_constraints(sos_to_update) - timer.stop('cons') - timer.start('vars') - if self._only_child_vars and config.update_vars: - vars_to_check = [] - for v_id, v in current_vars_dict.items(): - if v_id not in new_vars_set: - vars_to_check.append(v) - elif config.update_vars: - end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] - if config.update_vars: - vars_to_update = [] - for v in vars_to_check: - _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif (fixed is not v.fixed) or (fixed and (value != v.value)): - vars_to_update.append(v) - if self.update_config.treat_fixed_vars_as_params: - for c in self._referenced_variables[id(v)][0]: - cons_to_remove_and_add[c] = None - if self._referenced_variables[id(v)][2] is not None: - need_to_set_objective = True - elif domain_interval != v.domain.get_interval(): - vars_to_update.append(v) - self.update_variables(vars_to_update) - timer.stop('vars') - timer.start('cons') - cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) - self.remove_constraints(cons_to_remove_and_add) - self.add_constraints(cons_to_remove_and_add) - timer.stop('cons') - timer.start('named expressions') - if config.update_named_expressions: - cons_to_update = [] - for c, expr_list in self._named_expressions.items(): - if c in new_cons_set: - continue - for named_expr, old_expr in expr_list: - if named_expr.expr is not old_expr: - cons_to_update.append(c) - break - self.remove_constraints(cons_to_update) - self.add_constraints(cons_to_update) - for named_expr, old_expr in self._obj_named_expressions: - if named_expr.expr is not old_expr: - need_to_set_objective = True - break - timer.stop('named expressions') - timer.start('objective') - if self.update_config.check_for_new_objective: - pyomo_obj = get_objective(self._model) - if pyomo_obj is not self._objective: - need_to_set_objective = True - else: - pyomo_obj = self._objective - if self.update_config.update_objective: - if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: - need_to_set_objective = True - elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: - # we can definitely do something faster here than resetting the whole objective - need_to_set_objective = True - if need_to_set_objective: - self.set_objective(pyomo_obj) - timer.stop('objective') - - # this has to be done after the objective and constraints in case the - # old objective/constraints use old variables - timer.start('vars') - self.remove_variables(old_vars) - timer.stop('vars') - - -legacy_termination_condition_map = { - TerminationCondition.unknown: LegacyTerminationCondition.unknown, - TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, - TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, - TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, - TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, - TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, - TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, - TerminationCondition.error: LegacyTerminationCondition.error, - TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, - TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, -} - - -legacy_solver_status_map = { - TerminationCondition.unknown: LegacySolverStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, - TerminationCondition.iterationLimit: LegacySolverStatus.aborted, - TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, - TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, - TerminationCondition.unbounded: LegacySolverStatus.error, - TerminationCondition.provenInfeasible: LegacySolverStatus.error, - TerminationCondition.locallyInfeasible: LegacySolverStatus.error, - TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, - TerminationCondition.error: LegacySolverStatus.error, - TerminationCondition.interrupted: LegacySolverStatus.aborted, - TerminationCondition.licensingProblems: LegacySolverStatus.error, -} - - -legacy_solution_status_map = { - TerminationCondition.unknown: LegacySolutionStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, - TerminationCondition.unbounded: LegacySolutionStatus.unbounded, - TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, - TerminationCondition.error: LegacySolutionStatus.error, - TerminationCondition.interrupted: LegacySolutionStatus.error, - TerminationCondition.licensingProblems: LegacySolutionStatus.error, -} - - -class LegacySolverInterface: - def solve( - self, - model: _BlockData, - tee: bool = False, - load_solutions: bool = True, - logfile: Optional[str] = None, - solnfile: Optional[str] = None, - timelimit: Optional[float] = None, - report_timing: bool = False, - solver_io: Optional[str] = None, - suffixes: Optional[Sequence] = None, - options: Optional[Dict] = None, - keepfiles: bool = False, - symbolic_solver_labels: bool = False, - ): - original_config = self.config - self.config = self.config() - self.config.tee = tee - self.config.load_solution = load_solutions - self.config.symbolic_solver_labels = symbolic_solver_labels - self.config.time_limit = timelimit - self.config.report_timing = report_timing - if solver_io is not None: - raise NotImplementedError('Still working on this') - if suffixes is not None: - raise NotImplementedError('Still working on this') - if logfile is not None: - raise NotImplementedError('Still working on this') - if 'keepfiles' in self.config: - self.config.keepfiles = keepfiles - if solnfile is not None: - if 'filename' in self.config: - filename = os.path.splitext(solnfile)[0] - self.config.filename = filename - original_options = self.options - if options is not None: - self.options = options - - results: Results = super().solve(model) - - legacy_results = LegacySolverResults() - legacy_soln = LegacySolution() - legacy_results.solver.status = legacy_solver_status_map[ - results.termination_condition - ] - legacy_results.solver.termination_condition = legacy_termination_condition_map[ - results.termination_condition - ] - legacy_soln.status = legacy_solution_status_map[results.termination_condition] - legacy_results.solver.termination_message = str(results.termination_condition) - - obj = get_objective(model) - legacy_results.problem.sense = obj.sense - - if obj.sense == minimize: - legacy_results.problem.lower_bound = results.best_objective_bound - legacy_results.problem.upper_bound = results.best_feasible_objective - else: - legacy_results.problem.upper_bound = results.best_objective_bound - legacy_results.problem.lower_bound = results.best_feasible_objective - if ( - results.best_feasible_objective is not None - and results.best_objective_bound is not None - ): - legacy_soln.gap = abs( - results.best_feasible_objective - results.best_objective_bound - ) - else: - legacy_soln.gap = None - - symbol_map = SymbolMap() - symbol_map.byObject = dict(self.symbol_map.byObject) - symbol_map.bySymbol = dict(self.symbol_map.bySymbol) - symbol_map.aliases = dict(self.symbol_map.aliases) - symbol_map.default_labeler = self.symbol_map.default_labeler - model.solutions.add_symbol_map(symbol_map) - legacy_results._smap_id = id(symbol_map) - - delete_legacy_soln = True - if load_solutions: - if hasattr(model, 'dual') and model.dual.import_enabled(): - for c, val in results.solution_loader.get_duals().items(): - model.dual[c] = val - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - model.slack[c] = val - if hasattr(model, 'rc') and model.rc.import_enabled(): - for v, val in results.solution_loader.get_reduced_costs().items(): - model.rc[v] = val - elif results.best_feasible_objective is not None: - delete_legacy_soln = False - for v, val in results.solution_loader.get_primals().items(): - legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} - if hasattr(model, 'dual') and model.dual.import_enabled(): - for c, val in results.solution_loader.get_duals().items(): - legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - symbol = symbol_map.getSymbol(c) - if symbol in legacy_soln.constraint: - legacy_soln.constraint[symbol]['Slack'] = val - if hasattr(model, 'rc') and model.rc.import_enabled(): - for v, val in results.solution_loader.get_reduced_costs().items(): - legacy_soln.variable['Rc'] = val - - legacy_results.solution.insert(legacy_soln) - if delete_legacy_soln: - legacy_results.solution.delete(0) - - self.config = original_config - self.options = original_options - - return legacy_results - - def available(self, exception_flag=True): - ans = super().available() - if exception_flag and not ans: - raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') - return bool(ans) - - def license_is_valid(self) -> bool: - """Test if the solver license is valid on this system. - - Note that this method is included for compatibility with the - legacy SolverFactory interface. Unlicensed or open source - solvers will return True by definition. Licensed solvers will - return True if a valid license is found. - - Returns - ------- - available: bool - True if the solver license is valid. Otherwise, False. - - """ - return bool(self.available()) - - @property - def options(self): - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - return getattr(self, solver_name + '_options') - raise NotImplementedError('Could not find the correct options') - - @options.setter - def options(self, val): - found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - setattr(self, solver_name + '_options', val) - found = True - if not found: - raise NotImplementedError('Could not find the correct options') - - def __enter__(self): - return self - - def __exit__(self, t, v, traceback): - pass - - -class SolverFactoryClass(Factory): - def register(self, name, doc=None): - def decorator(cls): - self._cls[name] = cls - self._doc[name] = doc - - class LegacySolver(LegacySolverInterface, cls): - pass - - LegacySolverFactory.register(name, doc)(LegacySolver) - - return cls - - return decorator - - -SolverFactory = SolverFactoryClass() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index d907283f663..d65430e3c23 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,6 +1,7 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer +from pyomo.solver import base as solver_base def main(plot=True, n_points=200): @@ -31,7 +32,7 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied + assert res.termination_condition == solver_base.TerminationCondition.convergenceCriteriaSatisfied obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 22badd83d12..78137e790b6 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.contrib.appsi.base import PersistentBase +from pyomo.solver.base import PersistentBase from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -11,10 +11,9 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize +from pyomo.core.base.objective import _GeneralObjectiveData, minimize from pyomo.core.base.block import _BlockData from pyomo.core.base import SymbolMap, TextLabeler -from pyomo.common.errors import InfeasibleConstraintException class IntervalConfig(ConfigDict): diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 35071ab17ea..dd00089e84a 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,20 +1,16 @@ +import logging +import math +import subprocess +import sys +from typing import Optional, Sequence, Dict, List, Mapping + + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - InterfaceConfig, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.writers import LPWriter from pyomo.common.log import LogStream -import logging -import subprocess from pyomo.core.kernel.objective import minimize, maximize -import math from pyomo.common.collections import ComponentMap -from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -22,12 +18,13 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream -import sys -from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.config import InterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 7f9844fc21d..9f39528b0b0 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,29 +1,27 @@ -from pyomo.common.tempfiles import TempfileManager -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - MIPInterfaceConfig, - PersistentSolutionLoader, -) -from pyomo.contrib.appsi.writers import LPWriter import logging import math +import sys +import time +from typing import Optional, Sequence, Dict, List, Mapping + + +from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.appsi.writers import LPWriter +from pyomo.common.log import LogStream from pyomo.common.collections import ComponentMap -from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer -import sys -import time -from pyomo.common.log import LogStream from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 3f8eab638b0..8aaae4e31d4 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,7 +1,9 @@ from collections.abc import Iterable import logging import math +import sys from typing import List, Dict, Optional + from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import @@ -12,24 +14,19 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - MIPInterfaceConfig, - PersistentBase, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager -import sys +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader + logger = logging.getLogger(__name__) @@ -1196,8 +1193,8 @@ def set_linear_constraint_attr(self, con, attr, val): if attr in {'Sense', 'RHS', 'ConstrName'}: raise ValueError( 'Linear constraint attr {0} cannot be set with' - + ' the set_linear_constraint_attr method. Please use' - + ' the remove_constraint and add_constraint methods.'.format(attr) + ' the set_linear_constraint_attr method. Please use' + ' the remove_constraint and add_constraint methods.'.format(attr) ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True @@ -1225,8 +1222,8 @@ def set_var_attr(self, var, attr, val): if attr in {'LB', 'UB', 'VType', 'VarName'}: raise ValueError( 'Var attr {0} cannot be set with' - + ' the set_var_attr method. Please use' - + ' the update_var method.'.format(attr) + ' the set_var_attr method. Please use' + ' the update_var method.'.format(attr) ) if attr == 'Obj': raise ValueError( diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index e5c43d27c8d..c93d69527d8 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,5 +1,7 @@ import logging +import sys from typing import List, Dict, Optional + from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException @@ -16,18 +18,12 @@ from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - MIPInterfaceConfig, - PersistentBase, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -import sys +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index da42fc0be41..f754b5e85c0 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,18 +1,16 @@ +import math +import os +import sys +from typing import Dict +import logging +import subprocess + + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - InterfaceConfig, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.writers import NLWriter from pyomo.common.log import LogStream -import logging -import subprocess from pyomo.core.kernel.objective import minimize -import math from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions @@ -24,13 +22,13 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream -import sys -from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException -import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.config import InterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index fcff8916b5b..877d0971f2b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,11 +1,8 @@ -from pyomo.common.errors import PyomoException from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.contrib.appsi.base import TerminationCondition -from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.solver.base import TerminationCondition from pyomo.core.expr.taylor_series import taylor_series_expansion -from pyomo.contrib.appsi.cmodel import cmodel_available opt = Gurobi() diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 135f36d3695..fc97ba43fd0 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,7 +4,7 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py deleted file mode 100644 index 82a04b29e56..00000000000 --- a/pyomo/contrib/appsi/tests/test_base.py +++ /dev/null @@ -1,91 +0,0 @@ -from pyomo.common import unittest -from pyomo.contrib import appsi -import pyomo.environ as pe -from pyomo.core.base.var import ScalarVar - - -class TestResults(unittest.TestCase): - def test_uninitialized(self): - res = appsi.base.Results() - self.assertIsNone(res.best_feasible_objective) - self.assertIsNone(res.best_objective_bound) - self.assertEqual( - res.termination_condition, appsi.base.TerminationCondition.unknown - ) - - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have a valid solution.*' - ): - res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid slacks.*' - ): - res.solution_loader.get_slacks() - - def test_results(self): - m = pe.ConcreteModel() - m.x = ScalarVar() - m.y = ScalarVar() - m.c1 = pe.Constraint(expr=m.x == 1) - m.c2 = pe.Constraint(expr=m.y == 2) - - primals = {} - primals[id(m.x)] = (m.x, 1) - primals[id(m.y)] = (m.y, 2) - duals = {} - duals[m.c1] = 3 - duals[m.c2] = 4 - rc = {} - rc[id(m.x)] = (m.x, 5) - rc[id(m.y)] = (m.y, 6) - slacks = {} - slacks[m.c1] = 7 - slacks[m.c2] = 8 - - res = appsi.base.Results() - res.solution_loader = appsi.base.SolutionLoader( - primals=primals, duals=duals, slacks=slacks, reduced_costs=rc - ) - - res.solution_loader.load_vars() - self.assertAlmostEqual(m.x.value, 1) - self.assertAlmostEqual(m.y.value, 2) - - m.x.value = None - m.y.value = None - - res.solution_loader.load_vars([m.y]) - self.assertIsNone(m.x.value) - self.assertAlmostEqual(m.y.value, 2) - - duals2 = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - duals2 = res.solution_loader.get_duals([m.c2]) - self.assertNotIn(m.c1, duals2) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - rc2 = res.solution_loader.get_reduced_costs() - self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - rc2 = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, rc2) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - slacks2 = res.solution_loader.get_slacks() - self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) - - slacks2 = res.solution_loader.get_slacks([m.c2]) - self.assertNotIn(m.c1, slacks2) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 6a4a4ab2ff7..6ebc26b7b31 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -5,12 +5,10 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData -from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.core.expr.numvalue import value -from pyomo.contrib.appsi.base import PersistentBase from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer -from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.kernel.objective import minimize +from pyomo.solver.base import PersistentBase from .config import WriterConfig from ..cmodel import cmodel, cmodel_available diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index d0bb443508d..39aed3732aa 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,4 +1,6 @@ +import os from typing import List + from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData @@ -6,17 +8,15 @@ from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.core.expr.numvalue import value -from pyomo.contrib.appsi.base import PersistentBase -from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from .config import WriterConfig from pyomo.common.collections import OrderedSet -import os -from ..cmodel import cmodel, cmodel_available from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env +from pyomo.solver.base import PersistentBase +from .config import WriterConfig +from ..cmodel import cmodel, cmodel_available class NLWriter(PersistentBase): def __init__(self, only_child_vars=False): diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..2cd562edb2b 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,6 +30,7 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', + 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index 64c6452d06d..13b8b463662 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -9,6 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import util from . import base +from . import config from . import solution +from . import util + diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index b6d9e1592cb..8b39f387c92 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -45,7 +45,6 @@ ) from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap -from .cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant from pyomo.solver import ( @@ -138,29 +137,6 @@ class Results: the lower bound. For maximization problems, this is the upper bound. For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) - - Here is an example workflow: - - >>> import pyomo.environ as pe - >>> from pyomo.contrib import appsi - >>> m = pe.ConcreteModel() - >>> m.x = pe.Var() - >>> m.obj = pe.Objective(expr=m.x**2) - >>> opt = appsi.solvers.Ipopt() - >>> opt.config.load_solution = False - >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP - ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars() #doctest:+SKIP - ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP - ... elif results.best_feasible_objective is not None: #doctest:+SKIP - ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP - ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP - ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP - ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP - ... else: #doctest:+SKIP - ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP """ def __init__(self): @@ -426,39 +402,6 @@ def update_params(self): pass - -""" -What can change in a pyomo model? -- variables added or removed -- constraints added or removed -- objective changed -- objective expr changed -- params added or removed -- variable modified - - lb - - ub - - fixed or unfixed - - domain - - value -- constraint modified - - lower - - upper - - body - - active or not -- named expressions modified - - expr -- param modified - - value - -Ideas: -- Consider explicitly handling deactivated constraints; favor deactivation over removal - and activation over addition - -Notes: -- variable bounds cannot be updated with mutable params; you must call update_variables -""" - - class PersistentBase(abc.ABC): def __init__(self, only_child_vars=False): self._model = None @@ -480,7 +423,6 @@ def __init__(self, only_child_vars=False): self._vars_referenced_by_con = {} self._vars_referenced_by_obj = [] self._expr_types = None - self.use_extensions = False self._only_child_vars = only_child_vars @property @@ -496,8 +438,6 @@ def set_instance(self, model): self.__init__() self.update_config = saved_update_config self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() self.add_block(model) if self._objective is None: self.set_objective(None) @@ -561,10 +501,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): 'constraint {name} has already been added'.format(name=con.name) ) self._active_constraints[con] = (con.lower, con.body, con.upper) - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(con.body, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(con.body) + tmp = collect_vars_and_named_exprs(con.body) named_exprs, variables, fixed_vars, external_functions = tmp if not self._only_child_vars: self._check_for_new_vars(variables) @@ -617,10 +554,7 @@ def set_objective(self, obj: _GeneralObjectiveData): self._objective = obj self._objective_expr = obj.expr self._objective_sense = obj.sense - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(obj.expr) + tmp = collect_vars_and_named_exprs(obj.expr) named_exprs, variables, fixed_vars, external_functions = tmp if not self._only_child_vars: self._check_for_new_vars(variables) diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index e69de29bb2d..b5fcc4c4242 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -0,0 +1,91 @@ +from pyomo.common import unittest +from pyomo.solver import base +import pyomo.environ as pe +from pyomo.core.base.var import ScalarVar + + +class TestResults(unittest.TestCase): + def test_uninitialized(self): + res = base.Results() + self.assertIsNone(res.best_feasible_objective) + self.assertIsNone(res.best_objective_bound) + self.assertEqual( + res.termination_condition, base.TerminationCondition.unknown + ) + + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid slacks.*' + ): + res.solution_loader.get_slacks() + + def test_results(self): + m = pe.ConcreteModel() + m.x = ScalarVar() + m.y = ScalarVar() + m.c1 = pe.Constraint(expr=m.x == 1) + m.c2 = pe.Constraint(expr=m.y == 2) + + primals = {} + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = {} + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = {} + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + slacks = {} + slacks[m.c1] = 7 + slacks[m.c2] = 8 + + res = base.Results() + res.solution_loader = base.SolutionLoader( + primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + slacks2 = res.solution_loader.get_slacks() + self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) + + slacks2 = res.solution_loader.get_slacks([m.c2]) + self.assertNotIn(m.c1, slacks2) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) From 04d71cff3c3834c144b1462846c96b2fe6586953 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:03:25 -0600 Subject: [PATCH 0033/1178] Fix broken imports --- pyomo/solver/base.py | 13 +++++---- pyomo/solver/util.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8b39f387c92..7a451e59c11 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -27,8 +27,7 @@ from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.collections import ComponentMap -from .utils.get_objective import get_objective -from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs + from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -47,11 +46,11 @@ from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant -from pyomo.solver import ( - SolutionLoader, - SolutionLoaderBase, - UpdateConfig -) + +from pyomo.solver.config import UpdateConfig +from pyomo.solver.solution import SolutionLoader, SolutionLoaderBase +from pyomo.solver.util import get_objective, collect_vars_and_named_exprs + class TerminationCondition(enum.Enum): diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 8c768061678..4b8acf0de2e 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -9,6 +9,70 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.core.base.objective import Objective +from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types +import pyomo.core.expr as EXPR + + +def get_objective(block): + obj = None + for o in block.component_data_objects( + Objective, descend_into=True, active=True, sort=True + ): + if obj is not None: + raise ValueError('Multiple active objectives found') + obj = o + return obj + + +class _VarAndNamedExprCollector(ExpressionValueVisitor): + def __init__(self): + self.named_expressions = {} + self.variables = {} + self.fixed_vars = {} + self._external_functions = {} + + def visit(self, node, values): + pass + + def visiting_potential_leaf(self, node): + if type(node) in nonpyomo_leaf_types: + return True, None + + if node.is_variable_type(): + self.variables[id(node)] = node + if node.is_fixed(): + self.fixed_vars[id(node)] = node + return True, None + + if node.is_named_expression_type(): + self.named_expressions[id(node)] = node + return False, None + + if type(node) is EXPR.ExternalFunctionExpression: + self._external_functions[id(node)] = node + return False, None + + if node.is_expression_type(): + return False, None + + return True, None + + +_visitor = _VarAndNamedExprCollector() + + +def collect_vars_and_named_exprs(expr): + _visitor.__init__() + _visitor.dfs_postorder_stack(expr) + return ( + list(_visitor.named_expressions.values()), + list(_visitor.variables.values()), + list(_visitor.fixed_vars.values()), + list(_visitor._external_functions.values()), + ) + + class SolverUtils: pass From fe9cea4b8d2ec14a8b26ca0b566a5665f66640fb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:05:22 -0600 Subject: [PATCH 0034/1178] Trying to fix broken imports again --- pyomo/environ/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 2cd562edb2b..51c68449247 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,7 +30,6 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', - 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', From b41cf0089700e33741cd862f269fec668e6af490 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:07:59 -0600 Subject: [PATCH 0035/1178] Update plugins --- pyomo/contrib/appsi/plugins.py | 1 - pyomo/environ/__init__.py | 1 + pyomo/solver/plugins.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 pyomo/solver/plugins.py diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 5333158239e..75161e3548c 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,4 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from .base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..2cd562edb2b 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,6 +30,7 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', + 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py new file mode 100644 index 00000000000..926ac346f32 --- /dev/null +++ b/pyomo/solver/plugins.py @@ -0,0 +1 @@ +from .base import SolverFactory From 6a84fcf77aff7ef00206035a408130dc8a200ac5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:10:20 -0600 Subject: [PATCH 0036/1178] Trying again with plugins --- pyomo/solver/plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 926ac346f32..e15d1a585b1 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -1 +1,5 @@ from .base import SolverFactory + +def load(): + pass + From 2e1529828b3cfd6533f09936feccb5a27e92d1ad Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:12:39 -0600 Subject: [PATCH 0037/1178] PPlugins are my bane --- pyomo/contrib/appsi/plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 75161e3548c..86dcd298a93 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,4 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory +from pyomo.solver.base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder From 94be7450f7f35ace9ece2d78c4ffcf4d167ac891 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:27:01 -0600 Subject: [PATCH 0038/1178] Remove use_extensions attribute --- pyomo/contrib/appsi/solvers/gurobi.py | 2 -- pyomo/contrib/appsi/solvers/highs.py | 2 -- .../contrib/appsi/solvers/tests/test_persistent_solvers.py | 7 ------- 3 files changed, 11 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 8aaae4e31d4..a02b8c55170 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -498,8 +498,6 @@ def set_instance(self, model): ) self._reinit() self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index c93d69527d8..a1477125ca9 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -343,8 +343,6 @@ def set_instance(self, model): ) self._reinit() self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() self._solver_model = highspy.Highs() self.add_block(model) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index fc97ba43fd0..3629aeceb1e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1182,13 +1182,6 @@ def test_with_gdp( self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) - opt.use_extensions = True - res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) - @parameterized.expand(input=all_solvers) def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): opt: PersistentSolver = opt_class(only_child_vars=False) From 4017abcc127da5fb05e1dbfd9377cf544205e2d4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:40:00 -0600 Subject: [PATCH 0039/1178] Turn on pyomo.solver tests --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 0ac37747a65..ff8b5901189 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -602,7 +602,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo/contrib/appsi --junitxml="TEST-pyomo.xml" + pyomo/contrib/appsi pyomo/solver --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 From ee064d2f09fdf372f5dc76ac1b7395031b2dd842 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 11:17:41 -0600 Subject: [PATCH 0040/1178] SAVE POINT: about to mess with persistent base --- pyomo/solver/base.py | 279 ++++++++++++++++---------------- pyomo/solver/tests/test_base.py | 66 ++++++++ 2 files changed, 206 insertions(+), 139 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 7a451e59c11..510b61f7479 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -262,145 +262,6 @@ def is_persistent(self): return False -class PersistentSolver(SolverBase): - def is_persistent(self): - return True - - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: - """ - Load the solution of the primal variables into the value attribute of the variables. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. - """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): - v.set_value(val, skip_validation=True) - StaleFlagManager.mark_all_as_stale(delayed=True) - - @abc.abstractmethod - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - pass - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Declare sign convention in docstring here. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - duals: dict - Maps constraints to dual values - """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Parameters - ---------- - cons_to_load: list - A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slack values - """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Parameters - ---------- - vars_to_load: list - A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs - will be loaded. - - Returns - ------- - reduced_costs: ComponentMap - Maps variable to reduced cost - """ - raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) - ) - - @property - @abc.abstractmethod - def update_config(self) -> UpdateConfig: - pass - - @abc.abstractmethod - def set_instance(self, model): - pass - - @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def add_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def add_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def remove_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): - pass - - @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def update_params(self): - pass - - class PersistentBase(abc.ABC): def __init__(self, only_child_vars=False): self._model = None @@ -940,6 +801,146 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('vars') +class PersistentSolver(SolverBase): + def is_persistent(self): + return True + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slack values + """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) + + @property + @abc.abstractmethod + def update_config(self) -> UpdateConfig: + pass + + @abc.abstractmethod + def set_instance(self, model): + pass + + @abc.abstractmethod + def add_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def add_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def add_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def remove_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def remove_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def remove_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + pass + + @abc.abstractmethod + def update_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def update_params(self): + pass + + + # Everything below here preserves backwards compatibility legacy_termination_condition_map = { diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index b5fcc4c4242..3c389175d08 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -4,6 +4,72 @@ from pyomo.core.base.var import ScalarVar +class TestTerminationCondition(unittest.TestCase): + def test_member_list(self): + member_list = base.TerminationCondition._member_names_ + expected_list = ['unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(base.TerminationCondition.unknown.value, 42) + self.assertEqual(base.TerminationCondition.convergenceCriteriaSatisfied.value, 0) + self.assertEqual(base.TerminationCondition.maxTimeLimit.value, 1) + self.assertEqual(base.TerminationCondition.iterationLimit.value, 2) + self.assertEqual(base.TerminationCondition.objectiveLimit.value, 3) + self.assertEqual(base.TerminationCondition.minStepLength.value, 4) + self.assertEqual(base.TerminationCondition.unbounded.value, 5) + self.assertEqual(base.TerminationCondition.provenInfeasible.value, 6) + self.assertEqual(base.TerminationCondition.locallyInfeasible.value, 7) + self.assertEqual(base.TerminationCondition.infeasibleOrUnbounded.value, 8) + self.assertEqual(base.TerminationCondition.error.value, 9) + self.assertEqual(base.TerminationCondition.interrupted.value, 10) + self.assertEqual(base.TerminationCondition.licensingProblems.value, 11) + + +class TestSolutionStatus(unittest.TestCase): + def test_member_list(self): + member_list = base.SolutionStatus._member_names_ + expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(base.SolutionStatus.noSolution.value, 0) + self.assertEqual(base.SolutionStatus.infeasible.value, 10) + self.assertEqual(base.SolutionStatus.feasible.value, 20) + self.assertEqual(base.SolutionStatus.optimal.value, 30) + + +class TestSolverBase(unittest.TestCase): + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_solver_base(self): + self.instance = base.SolverBase() + self.assertFalse(self.instance.is_persistent()) + self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.config, None) + self.assertEqual(self.instance.solve(None), None) + self.assertEqual(self.instance.available(), None) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_solver_availability(self): + self.instance = base.SolverBase() + self.instance.Availability._value_ = 1 + self.assertTrue(self.instance.Availability.__bool__(self.instance.Availability)) + self.instance.Availability._value_ = -1 + self.assertFalse(self.instance.Availability.__bool__(self.instance.Availability)) + + class TestResults(unittest.TestCase): def test_uninitialized(self): res = base.Results() From 9bcec5d863e9bb529b09e00096fea87f2b1fd784 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 12:06:10 -0600 Subject: [PATCH 0041/1178] Rename PersistentSolver to PersistentSolverBase; PersistentBase to PersistentSolverUtils --- pyomo/contrib/appsi/fbbt.py | 4 +- pyomo/contrib/appsi/solvers/cbc.py | 4 +- pyomo/contrib/appsi/solvers/cplex.py | 4 +- pyomo/contrib/appsi/solvers/gurobi.py | 6 +- pyomo/contrib/appsi/solvers/highs.py | 6 +- pyomo/contrib/appsi/solvers/ipopt.py | 4 +- .../solvers/tests/test_persistent_solvers.py | 142 ++--- pyomo/contrib/appsi/writers/lp_writer.py | 4 +- pyomo/contrib/appsi/writers/nl_writer.py | 4 +- pyomo/solver/base.py | 554 +---------------- pyomo/solver/util.py | 555 +++++++++++++++++- 11 files changed, 646 insertions(+), 641 deletions(-) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 78137e790b6..cff1085de0d 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.solver.base import PersistentBase +from pyomo.solver.util import PersistentSolverUtils from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -59,7 +59,7 @@ def __init__( ) -class IntervalTightener(PersistentBase): +class IntervalTightener(PersistentSolverUtils): def __init__(self): super().__init__() self._config = IntervalConfig() diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index dd00089e84a..30250f66a86 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -22,7 +22,7 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import InterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader @@ -60,7 +60,7 @@ def __init__( self.log_level = logging.INFO -class Cbc(PersistentSolver): +class Cbc(PersistentSolverBase): def __init__(self, only_child_vars=False): self._config = CbcConfig() self._solver_options = {} diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 9f39528b0b0..0b1bd552370 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -19,7 +19,7 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader @@ -62,7 +62,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class Cplex(PersistentSolver): +class Cplex(PersistentSolverBase): _available = None def __init__(self, only_child_vars=False): diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index a02b8c55170..ba89c3e5d57 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -21,11 +21,11 @@ from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) @@ -221,7 +221,7 @@ def __init__(self): self.var2 = None -class Gurobi(PersistentBase, PersistentSolver): +class Gurobi(PersistentSolverUtils, PersistentSolverBase): """ Interface to Gurobi """ diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a1477125ca9..4a23d7c309a 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -18,12 +18,12 @@ from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ def update(self): self.highs.changeRowBounds(row_ndx, lb, ub) -class Highs(PersistentBase, PersistentSolver): +class Highs(PersistentSolverUtils, PersistentSolverBase): """ Interface to HiGHS """ diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index f754b5e85c0..467040a0967 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -26,7 +26,7 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import InterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader @@ -124,7 +124,7 @@ def __init__( } -class Ipopt(PersistentSolver): +class Ipopt(PersistentSolverBase): def __init__(self, only_child_vars=False): self._config = IpoptConfig() self._solver_options = {} diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 3629aeceb1e..1f357acf209 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,7 +4,7 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type @@ -78,10 +78,10 @@ def _load_tests(solver_list, only_child_vars_list): class TestSolvers(unittest.TestCase): @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): # this test is for issue #2888 - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -101,9 +101,9 @@ def test_remove_variable_and_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_stale_vars( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -146,9 +146,9 @@ def test_stale_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_range_constraint( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -169,9 +169,9 @@ def test_range_constraint( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -188,9 +188,9 @@ def test_reduced_costs( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -210,9 +210,9 @@ def test_reduced_costs2( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -244,13 +244,13 @@ def test_param_changes( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. """ - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -282,9 +282,9 @@ def test_immutable_param( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -316,9 +316,9 @@ def test_equality( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -352,9 +352,9 @@ def test_linear_expression( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_no_objective( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -386,9 +386,9 @@ def test_no_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -438,9 +438,9 @@ def test_add_remove_cons( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -485,8 +485,8 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + def test_duals(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -509,9 +509,9 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -533,9 +533,9 @@ def test_mutable_quadratic_coefficient( @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_objective( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -560,9 +560,9 @@ def test_mutable_quadratic_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) for treat_fixed_vars_as_params in [True, False]: opt.update_config.treat_fixed_vars_as_params = treat_fixed_vars_as_params if not opt.available(): @@ -600,9 +600,9 @@ def test_fixed_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_2( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -639,9 +639,9 @@ def test_fixed_vars_2( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_3( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -656,9 +656,9 @@ def test_fixed_vars_3( @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_fixed_vars_4( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -677,9 +677,9 @@ def test_fixed_vars_4( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_mutable_param_with_range( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest try: @@ -767,7 +767,7 @@ def test_mutable_param_with_range( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): @@ -815,7 +815,7 @@ def test_add_and_remove_vars( opt.load_vars([m.x]) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + def test_exp(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -829,7 +829,7 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + def test_log(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -844,9 +844,9 @@ def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_with_numpy( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -874,9 +874,9 @@ def test_with_numpy( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_bounds_with_params( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -908,9 +908,9 @@ def test_bounds_with_params( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -961,9 +961,9 @@ def test_solution_loader( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest from sys import platform @@ -1017,9 +1017,9 @@ def test_time_limit( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1078,9 +1078,9 @@ def test_objective_changes( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_domain( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1104,9 +1104,9 @@ def test_domain( @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_domain_with_integers( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1130,9 +1130,9 @@ def test_domain_with_integers( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_binaries( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1147,7 +1147,7 @@ def test_fixed_binaries( res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 1) - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) @@ -1158,9 +1158,9 @@ def test_fixed_binaries( @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1183,8 +1183,8 @@ def test_with_gdp( self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): - opt: PersistentSolver = opt_class(only_child_vars=False) + def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolverBase]): + opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1210,8 +1210,8 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]) self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver]): - opt: PersistentSolver = opt_class(only_child_vars=False) + def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverBase]): + opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1245,8 +1245,8 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + def test_bug_1(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1271,7 +1271,7 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_va @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestLegacySolverInterface(unittest.TestCase): @parameterized.expand(input=all_solvers) - def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): + def test_param_updates(self, name: str, opt_class: Type[PersistentSolverBase]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest @@ -1301,7 +1301,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) - def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): + def test_load_solutions(self, name: str, opt_class: Type[PersistentSolverBase]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 6ebc26b7b31..8deb92640c1 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -8,12 +8,12 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from pyomo.solver.base import PersistentBase +from pyomo.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available -class LPWriter(PersistentBase): +class LPWriter(PersistentSolverUtils): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 39aed3732aa..e853e22c96f 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,12 +13,12 @@ from pyomo.core.kernel.objective import minimize from pyomo.common.collections import OrderedSet from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.solver.base import PersistentBase +from pyomo.solver.base import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available -class NLWriter(PersistentBase): +class NLWriter(PersistentSolverUtils): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 510b61f7479..332f8cddff4 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -20,14 +20,11 @@ List, Tuple, ) -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var -from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.collections import ComponentMap - from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -45,11 +42,9 @@ from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager -from pyomo.core.expr.numvalue import NumericConstant - from pyomo.solver.config import UpdateConfig from pyomo.solver.solution import SolutionLoader, SolutionLoaderBase -from pyomo.solver.util import get_objective, collect_vars_and_named_exprs +from pyomo.solver.util import get_objective @@ -262,546 +257,7 @@ def is_persistent(self): return False -class PersistentBase(abc.ABC): - def __init__(self, only_child_vars=False): - self._model = None - self._active_constraints = {} # maps constraint to (lower, body, upper) - self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) - self._params = {} # maps param id to param - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._named_expressions = ( - {} - ) # maps constraint to list of tuples (named_expr, named_expr.expr) - self._external_functions = ComponentMap() - self._obj_named_expressions = [] - self._update_config = UpdateConfig() - self._referenced_variables = ( - {} - ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = {} - self._vars_referenced_by_obj = [] - self._expr_types = None - self._only_child_vars = only_child_vars - - @property - def update_config(self): - return self._update_config - - @update_config.setter - def update_config(self, val: UpdateConfig): - self._update_config = val - - def set_instance(self, model): - saved_update_config = self.update_config - self.__init__() - self.update_config = saved_update_config - self._model = model - self.add_block(model) - if self._objective is None: - self.set_objective(None) - - @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): - pass - - def add_variables(self, variables: List[_GeneralVarData]): - for v in variables: - if id(v) in self._referenced_variables: - raise ValueError( - 'variable {name} has already been added'.format(name=v.name) - ) - self._referenced_variables[id(v)] = [{}, {}, None] - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._add_variables(variables) - - @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): - pass - - def add_params(self, params: List[_ParamData]): - for p in params: - self._params[id(p)] = p - self._add_params(params) - - @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = {} - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - new_vars[v_id] = v - self.add_variables(list(new_vars.values())) - - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = {} - for v in variables: - v_id = id(v) - ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] - if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: - vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) - - def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = {} - for con in cons: - if con in self._named_expressions: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = (con.lower, con.body, con.upper) - tmp = collect_vars_and_named_exprs(con.body) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [(e, e.expr) for e in named_exprs] - if len(external_functions) > 0: - self._external_functions[con] = external_functions - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][0][con] = None - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - all_fixed_vars[id(v)] = v - self._add_constraints(cons) - for v in all_fixed_vars.values(): - v.fix() - - @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def add_sos_constraints(self, cons: List[_SOSConstraintData]): - for con in cons: - if con in self._vars_referenced_by_con: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = tuple() - variables = con.get_variables() - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [] - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][1][con] = None - self._add_sos_constraints(cons) - - @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): - pass - - def set_objective(self, obj: _GeneralObjectiveData): - if self._objective is not None: - for v in self._vars_referenced_by_obj: - self._referenced_variables[id(v)][2] = None - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_obj) - self._external_functions.pop(self._objective, None) - if obj is not None: - self._objective = obj - self._objective_expr = obj.expr - self._objective_sense = obj.sense - tmp = collect_vars_and_named_exprs(obj.expr) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._obj_named_expressions = [(i, i.expr) for i in named_exprs] - if len(external_functions) > 0: - self._external_functions[obj] = external_functions - self._vars_referenced_by_obj = variables - for v in variables: - self._referenced_variables[id(v)][2] = obj - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - self._set_objective(obj) - for v in fixed_vars: - v.fix() - else: - self._vars_referenced_by_obj = [] - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._obj_named_expressions = [] - self._set_objective(obj) - - def add_block(self, block): - param_dict = {} - for p in block.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) - if self._only_child_vars: - self.add_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects(Var, descend_into=True) - ).values() - ) - ) - self.add_constraints( - list(block.component_data_objects(Constraint, descend_into=True, active=True)) - ) - self.add_sos_constraints( - list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) - ) - obj = get_objective(block) - if obj is not None: - self.set_objective(obj) - - @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def remove_constraints(self, cons: List[_GeneralConstraintData]): - self._remove_constraints(cons) - for con in cons: - if con not in self._named_expressions: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][0].pop(con) - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - self._external_functions.pop(con, None) - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): - self._remove_sos_constraints(cons) - for con in cons: - if con not in self._vars_referenced_by_con: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][1].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): - pass - - def remove_variables(self, variables: List[_GeneralVarData]): - self._remove_variables(variables) - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - raise ValueError( - 'cannot remove variable {name} - it has not been added'.format( - name=v.name - ) - ) - cons_using, sos_using, obj_using = self._referenced_variables[v_id] - if cons_using or sos_using or (obj_using is not None): - raise ValueError( - 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( - name=v.name - ) - ) - del self._referenced_variables[v_id] - del self._vars[v_id] - - @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): - pass - - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) - for p in params: - del self._params[id(p)] - - def remove_block(self, block): - self.remove_constraints( - list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) - ) - self.remove_sos_constraints( - list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) - ) - if self._only_child_vars: - self.remove_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects( - ctype=Var, descend_into=True - ) - ).values() - ) - ) - self.remove_params( - list( - dict( - (id(p), p) - for p in block.component_data_objects( - ctype=Param, descend_into=True - ) - ).values() - ) - ) - - @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): - pass - - def update_variables(self, variables: List[_GeneralVarData]): - for v in variables: - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._update_variables(variables) - - @abc.abstractmethod - def update_params(self): - pass - - def update(self, timer: HierarchicalTimer = None): - if timer is None: - timer = HierarchicalTimer() - config = self.update_config - new_vars = [] - old_vars = [] - new_params = [] - old_params = [] - new_cons = [] - old_cons = [] - old_sos = [] - new_sos = [] - current_vars_dict = {} - current_cons_dict = {} - current_sos_dict = {} - timer.start('vars') - if self._only_child_vars and ( - config.check_for_new_or_removed_vars or config.update_vars - ): - current_vars_dict = { - id(v): v - for v in self._model.component_data_objects(Var, descend_into=True) - } - for v_id, v in current_vars_dict.items(): - if v_id not in self._vars: - new_vars.append(v) - for v_id, v_tuple in self._vars.items(): - if v_id not in current_vars_dict: - old_vars.append(v_tuple[0]) - elif config.update_vars: - start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - timer.stop('vars') - timer.start('params') - if config.check_for_new_or_removed_params: - current_params_dict = {} - for p in self._model.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - current_params_dict[id(_p)] = _p - for p_id, p in current_params_dict.items(): - if p_id not in self._params: - new_params.append(p) - for p_id, p in self._params.items(): - if p_id not in current_params_dict: - old_params.append(p) - timer.stop('params') - timer.start('cons') - if config.check_for_new_or_removed_constraints or config.update_constraints: - current_cons_dict = { - c: None - for c in self._model.component_data_objects( - Constraint, descend_into=True, active=True - ) - } - current_sos_dict = { - c: None - for c in self._model.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - } - for c in current_cons_dict.keys(): - if c not in self._vars_referenced_by_con: - new_cons.append(c) - for c in current_sos_dict.keys(): - if c not in self._vars_referenced_by_con: - new_sos.append(c) - for c in self._vars_referenced_by_con.keys(): - if c not in current_cons_dict and c not in current_sos_dict: - if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) - ): - old_cons.append(c) - else: - assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) - ) - old_sos.append(c) - self.remove_constraints(old_cons) - self.remove_sos_constraints(old_sos) - timer.stop('cons') - timer.start('params') - self.remove_params(old_params) - - # sticking this between removal and addition - # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() - - self.add_params(new_params) - timer.stop('params') - timer.start('vars') - self.add_variables(new_vars) - timer.stop('vars') - timer.start('cons') - self.add_constraints(new_cons) - self.add_sos_constraints(new_sos) - new_cons_set = set(new_cons) - new_sos_set = set(new_sos) - new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = {} - need_to_set_objective = False - if config.update_constraints: - cons_to_update = [] - sos_to_update = [] - for c in current_cons_dict.keys(): - if c not in new_cons_set: - cons_to_update.append(c) - for c in current_sos_dict.keys(): - if c not in new_sos_set: - sos_to_update.append(c) - for c in cons_to_update: - lower, body, upper = self._active_constraints[c] - new_lower, new_body, new_upper = c.lower, c.body, c.upper - if new_body is not body: - cons_to_remove_and_add[c] = None - continue - if new_lower is not lower: - if ( - type(new_lower) is NumericConstant - and type(lower) is NumericConstant - and new_lower.value == lower.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - if new_upper is not upper: - if ( - type(new_upper) is NumericConstant - and type(upper) is NumericConstant - and new_upper.value == upper.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - self.remove_sos_constraints(sos_to_update) - self.add_sos_constraints(sos_to_update) - timer.stop('cons') - timer.start('vars') - if self._only_child_vars and config.update_vars: - vars_to_check = [] - for v_id, v in current_vars_dict.items(): - if v_id not in new_vars_set: - vars_to_check.append(v) - elif config.update_vars: - end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] - if config.update_vars: - vars_to_update = [] - for v in vars_to_check: - _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif (fixed is not v.fixed) or (fixed and (value != v.value)): - vars_to_update.append(v) - if self.update_config.treat_fixed_vars_as_params: - for c in self._referenced_variables[id(v)][0]: - cons_to_remove_and_add[c] = None - if self._referenced_variables[id(v)][2] is not None: - need_to_set_objective = True - elif domain_interval != v.domain.get_interval(): - vars_to_update.append(v) - self.update_variables(vars_to_update) - timer.stop('vars') - timer.start('cons') - cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) - self.remove_constraints(cons_to_remove_and_add) - self.add_constraints(cons_to_remove_and_add) - timer.stop('cons') - timer.start('named expressions') - if config.update_named_expressions: - cons_to_update = [] - for c, expr_list in self._named_expressions.items(): - if c in new_cons_set: - continue - for named_expr, old_expr in expr_list: - if named_expr.expr is not old_expr: - cons_to_update.append(c) - break - self.remove_constraints(cons_to_update) - self.add_constraints(cons_to_update) - for named_expr, old_expr in self._obj_named_expressions: - if named_expr.expr is not old_expr: - need_to_set_objective = True - break - timer.stop('named expressions') - timer.start('objective') - if self.update_config.check_for_new_objective: - pyomo_obj = get_objective(self._model) - if pyomo_obj is not self._objective: - need_to_set_objective = True - else: - pyomo_obj = self._objective - if self.update_config.update_objective: - if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: - need_to_set_objective = True - elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: - # we can definitely do something faster here than resetting the whole objective - need_to_set_objective = True - if need_to_set_objective: - self.set_objective(pyomo_obj) - timer.stop('objective') - - # this has to be done after the objective and constraints in case the - # old objective/constraints use old variables - timer.start('vars') - self.remove_variables(old_vars) - timer.stop('vars') - - -class PersistentSolver(SolverBase): +class PersistentSolverBase(SolverBase): def is_persistent(self): return True diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 4b8acf0de2e..fa2782f6bc4 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -9,9 +9,20 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.core.base.objective import Objective +import abc +from typing import List + from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types import pyomo.core.expr as EXPR +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.objective import Objective, _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.solver.config import UpdateConfig def get_objective(block): @@ -76,12 +87,550 @@ def collect_vars_and_named_exprs(expr): class SolverUtils: pass + class SubprocessSolverUtils: pass + class DirectSolverUtils: pass -class PersistentSolverUtils: - pass + +class PersistentSolverUtils(abc.ABC): + def __init__(self, only_child_vars=False): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._update_config = UpdateConfig() + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + self._only_child_vars = only_child_vars + + @property + def update_config(self): + return self._update_config + + @update_config.setter + def update_config(self, val: UpdateConfig): + self._update_config = val + + def set_instance(self, model): + saved_update_config = self.update_config + self.__init__() + self.update_config = saved_update_config + self._model = model + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + if self._only_child_vars: + self.add_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects(Var, descend_into=True) + ).values() + ) + ) + self.add_constraints( + list(block.component_data_objects(Constraint, descend_into=True, active=True)) + ) + self.add_sos_constraints( + list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) + ) + self.remove_sos_constraints( + list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) + ) + if self._only_child_vars: + self.remove_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects( + ctype=Var, descend_into=True + ) + ).values() + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.update_config + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if self._only_child_vars and ( + config.check_for_new_or_removed_vars or config.update_vars + ): + current_vars_dict = { + id(v): v + for v in self._model.component_data_objects(Var, descend_into=True) + } + for v_id, v in current_vars_dict.items(): + if v_id not in self._vars: + new_vars.append(v) + for v_id, v_tuple in self._vars.items(): + if v_id not in current_vars_dict: + old_vars.append(v_tuple[0]) + elif config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if self._only_child_vars and config.update_vars: + vars_to_check = [] + for v_id, v in current_vars_dict.items(): + if v_id not in new_vars_set: + vars_to_check.append(v) + elif config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif (fixed is not v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.update_config.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.update_config.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.update_config.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') From 54fa01c9d1d280ae24e207e8407752322a0ed69a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 12:09:40 -0600 Subject: [PATCH 0042/1178] Correct broken import --- pyomo/contrib/appsi/writers/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index e853e22c96f..f6edc076b04 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.kernel.objective import minimize from pyomo.common.collections import OrderedSet from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.solver.base import PersistentSolverUtils +from pyomo.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available From 511a54cfa75a63bba431aebd7cd13b1b531f3767 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 12:44:36 -0600 Subject: [PATCH 0043/1178] Add in util tests; reformat with black --- pyomo/contrib/appsi/build.py | 2 +- .../contrib/appsi/examples/getting_started.py | 5 +- pyomo/contrib/appsi/solvers/cbc.py | 12 +- pyomo/contrib/appsi/solvers/cplex.py | 9 +- pyomo/contrib/appsi/solvers/gurobi.py | 25 +-- pyomo/contrib/appsi/solvers/highs.py | 24 ++- pyomo/contrib/appsi/solvers/ipopt.py | 12 +- .../solvers/tests/test_gurobi_persistent.py | 4 +- .../solvers/tests/test_persistent_solvers.py | 148 +++++++++++++----- pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/nl_writer.py | 1 + pyomo/solver/__init__.py | 1 - pyomo/solver/base.py | 14 +- pyomo/solver/config.py | 7 +- pyomo/solver/plugins.py | 2 +- pyomo/solver/solution.py | 13 +- pyomo/solver/tests/test_base.py | 51 +++--- pyomo/solver/tests/test_config.py | 10 ++ pyomo/solver/tests/test_solution.py | 10 ++ pyomo/solver/tests/test_util.py | 75 +++++++++ pyomo/solver/util.py | 23 ++- 21 files changed, 329 insertions(+), 121 deletions(-) diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 37826cf85fb..3d37135665a 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -116,7 +116,7 @@ def run(self): pybind11.setup_helpers.MACOS = original_pybind11_setup_helpers_macos -class AppsiBuilder(): +class AppsiBuilder: def __call__(self, parallel): return build_appsi() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index d65430e3c23..de5357776f4 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -32,7 +32,10 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == solver_base.TerminationCondition.convergenceCriteriaSatisfied + assert ( + res.termination_condition + == solver_base.TerminationCondition.convergenceCriteriaSatisfied + ) obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 30250f66a86..021ff76217d 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -229,7 +229,9 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: results.termination_condition = TerminationCondition.provenInfeasible @@ -304,7 +306,8 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): @@ -313,7 +316,10 @@ def _parse_soln(self): results.best_feasible_objective = None else: results.best_feasible_objective = obj_val - elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: + elif ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 0b1bd552370..759bd7ff9d5 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -282,7 +282,9 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: @@ -334,7 +336,10 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'results.best_feasible_objective before loading a solution.' ) else: - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + ): logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index ba89c3e5d57..c2db835922d 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -97,7 +97,7 @@ def __init__(self, solver): self.solution_loader = GurobiSolutionLoader(solver=solver) -class _MutableLowerBound(): +class _MutableLowerBound: def __init__(self, expr): self.var = None self.expr = expr @@ -106,7 +106,7 @@ def update(self): self.var.setAttr('lb', value(self.expr)) -class _MutableUpperBound(): +class _MutableUpperBound: def __init__(self, expr): self.var = None self.expr = expr @@ -115,7 +115,7 @@ def update(self): self.var.setAttr('ub', value(self.expr)) -class _MutableLinearCoefficient(): +class _MutableLinearCoefficient: def __init__(self): self.expr = None self.var = None @@ -126,7 +126,7 @@ def update(self): self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) -class _MutableRangeConstant(): +class _MutableRangeConstant: def __init__(self): self.lhs_expr = None self.rhs_expr = None @@ -142,7 +142,7 @@ def update(self): slack.ub = rhs_val - lhs_val -class _MutableConstant(): +class _MutableConstant: def __init__(self): self.expr = None self.con = None @@ -151,7 +151,7 @@ def update(self): self.con.rhs = value(self.expr) -class _MutableQuadraticConstraint(): +class _MutableQuadraticConstraint: def __init__( self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs ): @@ -186,7 +186,7 @@ def get_updated_rhs(self): return value(self.constant.expr) -class _MutableObjective(): +class _MutableObjective: def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant = constant @@ -214,7 +214,7 @@ def get_updated_expression(self): return gurobi_expr -class _MutableQuadraticCoefficient(): +class _MutableQuadraticCoefficient: def __init__(self): self.expr = None self.var1 = None @@ -869,7 +869,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.provenInfeasible elif status == grb.INF_OR_UNBD: @@ -920,7 +922,10 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + ): logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 4a23d7c309a..3b7c92ed9e8 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -67,7 +67,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class _MutableVarBounds(): +class _MutableVarBounds: def __init__(self, lower_expr, upper_expr, pyomo_var_id, var_map, highs): self.pyomo_var_id = pyomo_var_id self.lower_expr = lower_expr @@ -82,7 +82,7 @@ def update(self): self.highs.changeColBounds(col_ndx, lb, ub) -class _MutableLinearCoefficient(): +class _MutableLinearCoefficient: def __init__(self, pyomo_con, pyomo_var_id, con_map, var_map, expr, highs): self.expr = expr self.highs = highs @@ -97,7 +97,7 @@ def update(self): self.highs.changeCoeff(row_ndx, col_ndx, value(self.expr)) -class _MutableObjectiveCoefficient(): +class _MutableObjectiveCoefficient: def __init__(self, pyomo_var_id, var_map, expr, highs): self.expr = expr self.highs = highs @@ -109,7 +109,7 @@ def update(self): self.highs.changeColCost(col_ndx, value(self.expr)) -class _MutableObjectiveOffset(): +class _MutableObjectiveOffset: def __init__(self, expr, highs): self.expr = expr self.highs = highs @@ -118,7 +118,7 @@ def update(self): self.highs.changeObjectiveOffset(value(self.expr)) -class _MutableConstraintBounds(): +class _MutableConstraintBounds: def __init__(self, lower_expr, upper_expr, pyomo_con, con_map, highs): self.lower_expr = lower_expr self.upper_expr = upper_expr @@ -604,7 +604,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status == highspy.HighsModelStatus.kInfeasible: results.termination_condition = TerminationCondition.provenInfeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: @@ -627,7 +629,10 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, @@ -639,7 +644,10 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + ): logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 467040a0967..6c4b7601d2c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -300,7 +300,9 @@ def _parse_sol(self): termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif 'Problem may be infeasible' in termination_line: results.termination_condition = TerminationCondition.locallyInfeasible elif 'problem might be unbounded' in termination_line: @@ -381,7 +383,8 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v, val in self._primal_sol.items(): @@ -392,7 +395,10 @@ def _parse_sol(self): results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: + elif ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 877d0971f2b..9fdce87b8de 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -157,7 +157,9 @@ def test_lp(self): res = opt.solve(self.m) self.assertAlmostEqual(x + y, res.best_feasible_objective) self.assertAlmostEqual(x + y, res.best_objective_bound) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 1f357acf209..bf92244ec36 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -18,11 +18,7 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [ - ('gurobi', Gurobi), - ('ipopt', Ipopt), - ('highs', Highs), -] +all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('highs', Highs)] mip_solvers = [('gurobi', Gurobi), ('highs', Highs)] nlp_solvers = [('ipopt', Ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] @@ -88,7 +84,9 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -96,7 +94,9 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) @@ -156,13 +156,17 @@ def test_range_constraint( m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -179,7 +183,9 @@ def test_reduced_costs( m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -197,13 +203,17 @@ def test_reduced_costs2( m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -233,7 +243,10 @@ def test_param_changes( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -271,7 +284,10 @@ def test_immutable_param( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -305,7 +321,10 @@ def test_equality( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -345,7 +364,10 @@ def test_linear_expression( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) @@ -375,7 +397,10 @@ def test_no_objective( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) @@ -404,7 +429,9 @@ def test_add_remove_cons( m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -415,7 +442,9 @@ def test_add_remove_cons( m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -427,7 +456,9 @@ def test_add_remove_cons( del m.c3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -453,7 +484,9 @@ def test_results_infeasible( res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertNotEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) if opt_class is Ipopt: acceptable_termination_conditions = { TerminationCondition.provenInfeasible, @@ -485,7 +518,9 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_duals(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_duals( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -747,7 +782,10 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) @@ -784,7 +822,9 @@ def test_add_and_remove_vars( opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -798,7 +838,9 @@ def test_add_and_remove_vars( opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -807,7 +849,9 @@ def test_add_and_remove_vars( opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -815,7 +859,9 @@ def test_add_and_remove_vars( opt.load_vars([m.x]) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_exp(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_exp( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -829,7 +875,9 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolverBase], only_child_ self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_log(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_log( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -868,7 +916,9 @@ def test_with_numpy( ) ) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -1011,9 +1061,7 @@ def test_time_limit( opt.config.time_limit = 0 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.maxTimeLimit - ) + self.assertEqual(res.termination_condition, TerminationCondition.maxTimeLimit) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( @@ -1183,7 +1231,9 @@ def test_with_gdp( self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_variables_elsewhere( + self, name: str, opt_class: Type[PersistentSolverBase] + ): opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1197,20 +1247,26 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolverBa m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_variables_elsewhere2( + self, name: str, opt_class: Type[PersistentSolverBase] + ): opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1227,7 +1283,9 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverB m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1237,7 +1295,9 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverB del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1245,7 +1305,9 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverB self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_1(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_bug_1( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1259,12 +1321,16 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolverBase], only_chil m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 3) diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 4376b9284fa..2a4e638f097 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,3 @@ -class WriterConfig(): +class WriterConfig: def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index f6edc076b04..1be657ba762 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -18,6 +18,7 @@ from .config import WriterConfig from ..cmodel import cmodel, cmodel_available + class NLWriter(PersistentSolverUtils): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index 13b8b463662..a3c2e0e95e8 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -13,4 +13,3 @@ from . import config from . import solution from . import util - diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 332f8cddff4..f0a07d0aca3 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -11,15 +11,7 @@ import abc import enum -from typing import ( - Sequence, - Dict, - Optional, - Mapping, - NoReturn, - List, - Tuple, -) +from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -47,7 +39,6 @@ from pyomo.solver.util import get_objective - class TerminationCondition(enum.Enum): """ An enumeration for checking the termination condition of solvers @@ -97,7 +88,7 @@ class SolutionStatus(enum.IntEnum): """ An enumeration for interpreting the result of a termination. This describes the designated status by the solver to be loaded back into the model. - + For now, we are choosing to use IntEnum such that return values are numerically assigned in increasing order. """ @@ -396,7 +387,6 @@ def update_params(self): pass - # Everything below here preserves backwards compatibility legacy_termination_condition_map = { diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index ab9c30a0549..f446dc714db 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -10,7 +10,12 @@ # ___________________________________________________________________________ from typing import Optional -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeFloat, + NonNegativeInt, +) class InterfaceConfig(ConfigDict): diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index e15d1a585b1..7e479474605 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -1,5 +1,5 @@ from .base import SolverFactory + def load(): pass - diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py index 2d422736f2c..1ef79050701 100644 --- a/pyomo/solver/solution.py +++ b/pyomo/solver/solution.py @@ -10,14 +10,7 @@ # ___________________________________________________________________________ import abc -from typing import ( - Sequence, - Dict, - Optional, - Mapping, - MutableMapping, - NoReturn, -) +from typing import Sequence, Dict, Optional, Mapping, MutableMapping, NoReturn from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData @@ -250,7 +243,3 @@ def get_reduced_costs( def invalidate(self): self._valid = False - - - - diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 3c389175d08..dcbe13e8230 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.solver import base import pyomo.environ as pe @@ -7,24 +18,28 @@ class TestTerminationCondition(unittest.TestCase): def test_member_list(self): member_list = base.TerminationCondition._member_names_ - expected_list = ['unknown', - 'convergenceCriteriaSatisfied', - 'maxTimeLimit', - 'iterationLimit', - 'objectiveLimit', - 'minStepLength', - 'unbounded', - 'provenInfeasible', - 'locallyInfeasible', - 'infeasibleOrUnbounded', - 'error', - 'interrupted', - 'licensingProblems'] + expected_list = [ + 'unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems', + ] self.assertEqual(member_list, expected_list) def test_codes(self): self.assertEqual(base.TerminationCondition.unknown.value, 42) - self.assertEqual(base.TerminationCondition.convergenceCriteriaSatisfied.value, 0) + self.assertEqual( + base.TerminationCondition.convergenceCriteriaSatisfied.value, 0 + ) self.assertEqual(base.TerminationCondition.maxTimeLimit.value, 1) self.assertEqual(base.TerminationCondition.iterationLimit.value, 2) self.assertEqual(base.TerminationCondition.objectiveLimit.value, 3) @@ -67,7 +82,9 @@ def test_solver_availability(self): self.instance.Availability._value_ = 1 self.assertTrue(self.instance.Availability.__bool__(self.instance.Availability)) self.instance.Availability._value_ = -1 - self.assertFalse(self.instance.Availability.__bool__(self.instance.Availability)) + self.assertFalse( + self.instance.Availability.__bool__(self.instance.Availability) + ) class TestResults(unittest.TestCase): @@ -75,9 +92,7 @@ def test_uninitialized(self): res = base.Results() self.assertIsNone(res.best_feasible_objective) self.assertIsNone(res.best_objective_bound) - self.assertEqual( - res.termination_condition, base.TerminationCondition.unknown - ) + self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/solver/tests/test_solution.py +++ b/pyomo/solver/tests/test_solution.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/solver/tests/test_util.py b/pyomo/solver/tests/test_util.py index e69de29bb2d..737a271d603 100644 --- a/pyomo/solver/tests/test_util.py +++ b/pyomo/solver/tests/test_util.py @@ -0,0 +1,75 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +import pyomo.environ as pyo +from pyomo.solver.util import collect_vars_and_named_exprs, get_objective +from typing import Callable +from pyomo.common.gsl import find_GSL + + +class TestGenericUtils(unittest.TestCase): + def basics_helper(self, collector: Callable, *args): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.E = pyo.Expression(expr=2 * m.z + 1) + m.y.fix(3) + e = m.x * m.y + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.x, m.y, m.z], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([], external_funcs) + + def test_collect_vars_basics(self): + self.basics_helper(collect_vars_and_named_exprs) + + def external_func_helper(self, collector: Callable, *args): + DLL = find_GSL() + if not DLL: + self.skipTest('Could not find amplgsl.dll library') + + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.hypot = pyo.ExternalFunction(library=DLL, function='gsl_hypot') + func = m.hypot(m.x, m.x * m.y) + m.E = pyo.Expression(expr=2 * func) + m.y.fix(3) + e = m.z + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.z, m.x, m.y], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([func], external_funcs) + + def test_collect_vars_external(self): + self.external_func_helper(collect_vars_and_named_exprs) + + def simple_model(self): + model = pyo.ConcreteModel() + model.x = pyo.Var([1, 2], domain=pyo.NonNegativeReals) + model.OBJ = pyo.Objective(expr=2 * model.x[1] + 3 * model.x[2]) + model.Constraint1 = pyo.Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) + return model + + def test_get_objective_success(self): + model = self.simple_model() + self.assertEqual(model.OBJ, get_objective(model)) + + def test_get_objective_raise(self): + model = self.simple_model() + model.OBJ2 = pyo.Objective(expr=model.x[1] - 4 * model.x[2]) + with self.assertRaises(ValueError): + get_objective(model) diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index fa2782f6bc4..1fb1738470b 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -289,10 +289,16 @@ def add_block(self, block): ) ) self.add_constraints( - list(block.component_data_objects(Constraint, descend_into=True, active=True)) + list( + block.component_data_objects(Constraint, descend_into=True, active=True) + ) ) self.add_sos_constraints( - list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) + list( + block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ) ) obj = get_objective(block) if obj is not None: @@ -375,10 +381,18 @@ def remove_params(self, params: List[_ParamData]): def remove_block(self, block): self.remove_constraints( - list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) + list( + block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ) ) self.remove_sos_constraints( - list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) + list( + block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ) ) if self._only_child_vars: self.remove_variables( @@ -633,4 +647,3 @@ def update(self, timer: HierarchicalTimer = None): timer.start('vars') self.remove_variables(old_vars) timer.stop('vars') - From 6cf3f8221761e6f97ebe0b92695debf78312ebf6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:41:44 -0600 Subject: [PATCH 0044/1178] Add more unit tests --- pyomo/solver/tests/test_base.py | 49 +++++++++++++++++++++++++++++++ pyomo/solver/tests/test_config.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index dcbe13e8230..355941a1eb1 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -87,6 +87,55 @@ def test_solver_availability(self): ) +class TestPersistentSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['remove_params', + 'version', + 'config', + 'update_variables', + 'remove_variables', + 'add_constraints', + 'get_primals', + 'set_instance', + 'set_objective', + 'update_params', + 'remove_block', + 'add_block', + 'available', + 'update_config', + 'add_params', + 'remove_constraints', + 'add_variables', + 'solve'] + member_list = list(base.PersistentSolverBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) + def test_persistent_solver_base(self): + self.instance = base.PersistentSolverBase() + self.assertTrue(self.instance.is_persistent()) + self.assertEqual(self.instance.get_primals(), None) + self.assertEqual(self.instance.update_config, None) + self.assertEqual(self.instance.set_instance(None), None) + self.assertEqual(self.instance.add_variables(None), None) + self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_constraints(None), None) + self.assertEqual(self.instance.add_block(None), None) + self.assertEqual(self.instance.remove_variables(None), None) + self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_constraints(None), None) + self.assertEqual(self.instance.remove_block(None), None) + self.assertEqual(self.instance.set_objective(None), None) + self.assertEqual(self.instance.update_variables(None), None) + self.assertEqual(self.instance.update_params(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_duals() + with self.assertRaises(NotImplementedError): + self.instance.get_slacks() + with self.assertRaises(NotImplementedError): + self.instance.get_reduced_costs() + + class TestResults(unittest.TestCase): def test_uninitialized(self): res = base.Results() diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index d93cfd77b3c..378facb58d2 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -8,3 +8,50 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.solver.config import InterfaceConfig, MIPInterfaceConfig + +class TestInterfaceConfig(unittest.TestCase): + + def test_interface_default_instantiation(self): + config = InterfaceConfig() + self.assertEqual(config._description, None) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solution) + self.assertFalse(config.symbolic_solver_labels) + self.assertFalse(config.report_timing) + + def test_interface_custom_instantiation(self): + config = InterfaceConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + config.time_limit = 1.0 + self.assertEqual(config.time_limit, 1.0) + + +class TestMIPInterfaceConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = MIPInterfaceConfig() + self.assertEqual(config._description, None) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solution) + self.assertFalse(config.symbolic_solver_labels) + self.assertFalse(config.report_timing) + self.assertEqual(config.mip_gap, None) + self.assertFalse(config.relax_integrality) + + def test_interface_custom_instantiation(self): + config = MIPInterfaceConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + config.time_limit = 1.0 + self.assertEqual(config.time_limit, 1.0) + config.mip_gap = 2.5 + self.assertEqual(config.mip_gap, 2.5) From 4d3191aa11d5f69695e4ea469655a896b09f35d1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:49:07 -0600 Subject: [PATCH 0045/1178] Add more unit tests --- pyomo/solver/tests/test_solution.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py index d93cfd77b3c..c4c2f790b55 100644 --- a/pyomo/solver/tests/test_solution.py +++ b/pyomo/solver/tests/test_solution.py @@ -8,3 +8,23 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.solver import solution + +class TestPersistentSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['get_primals'] + member_list = list(solution.SolutionLoaderBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + @unittest.mock.patch.multiple(solution.SolutionLoaderBase, __abstractmethods__=set()) + def test_solution_loader_base(self): + self.instance = solution.SolutionLoaderBase() + self.assertEqual(self.instance.get_primals(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_duals() + with self.assertRaises(NotImplementedError): + self.instance.get_slacks() + with self.assertRaises(NotImplementedError): + self.instance.get_reduced_costs() From 9dffd2605bd6e7316a3e7952cbca5793cb4af7db Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:50:48 -0600 Subject: [PATCH 0046/1178] Remove APPSI utils -> have been moved to pyomo.solver.util --- pyomo/contrib/appsi/utils/__init__.py | 2 - .../utils/collect_vars_and_named_exprs.py | 50 ----------------- pyomo/contrib/appsi/utils/get_objective.py | 12 ---- pyomo/contrib/appsi/utils/tests/__init__.py | 0 .../test_collect_vars_and_named_exprs.py | 56 ------------------- 5 files changed, 120 deletions(-) delete mode 100644 pyomo/contrib/appsi/utils/__init__.py delete mode 100644 pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py delete mode 100644 pyomo/contrib/appsi/utils/get_objective.py delete mode 100644 pyomo/contrib/appsi/utils/tests/__init__.py delete mode 100644 pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py deleted file mode 100644 index f665736fd4a..00000000000 --- a/pyomo/contrib/appsi/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .get_objective import get_objective -from .collect_vars_and_named_exprs import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py deleted file mode 100644 index bfbbf5aecdf..00000000000 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ /dev/null @@ -1,50 +0,0 @@ -from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types -import pyomo.core.expr as EXPR - - -class _VarAndNamedExprCollector(ExpressionValueVisitor): - def __init__(self): - self.named_expressions = {} - self.variables = {} - self.fixed_vars = {} - self._external_functions = {} - - def visit(self, node, values): - pass - - def visiting_potential_leaf(self, node): - if type(node) in nonpyomo_leaf_types: - return True, None - - if node.is_variable_type(): - self.variables[id(node)] = node - if node.is_fixed(): - self.fixed_vars[id(node)] = node - return True, None - - if node.is_named_expression_type(): - self.named_expressions[id(node)] = node - return False, None - - if type(node) is EXPR.ExternalFunctionExpression: - self._external_functions[id(node)] = node - return False, None - - if node.is_expression_type(): - return False, None - - return True, None - - -_visitor = _VarAndNamedExprCollector() - - -def collect_vars_and_named_exprs(expr): - _visitor.__init__() - _visitor.dfs_postorder_stack(expr) - return ( - list(_visitor.named_expressions.values()), - list(_visitor.variables.values()), - list(_visitor.fixed_vars.values()), - list(_visitor._external_functions.values()), - ) diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py deleted file mode 100644 index 30dd911f9c8..00000000000 --- a/pyomo/contrib/appsi/utils/get_objective.py +++ /dev/null @@ -1,12 +0,0 @@ -from pyomo.core.base.objective import Objective - - -def get_objective(block): - obj = None - for o in block.component_data_objects( - Objective, descend_into=True, active=True, sort=True - ): - if obj is not None: - raise ValueError('Multiple active objectives found') - obj = o - return obj diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py deleted file mode 100644 index 4c2a167a017..00000000000 --- a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py +++ /dev/null @@ -1,56 +0,0 @@ -from pyomo.common import unittest -import pyomo.environ as pe -from pyomo.contrib.appsi.utils import collect_vars_and_named_exprs -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available -from typing import Callable -from pyomo.common.gsl import find_GSL - - -class TestCollectVarsAndNamedExpressions(unittest.TestCase): - def basics_helper(self, collector: Callable, *args): - m = pe.ConcreteModel() - m.x = pe.Var() - m.y = pe.Var() - m.z = pe.Var() - m.E = pe.Expression(expr=2 * m.z + 1) - m.y.fix(3) - e = m.x * m.y + m.x * m.E - named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) - self.assertEqual([m.E], named_exprs) - self.assertEqual([m.x, m.y, m.z], var_list) - self.assertEqual([m.y], fixed_vars) - self.assertEqual([], external_funcs) - - def test_basics(self): - self.basics_helper(collect_vars_and_named_exprs) - - @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') - def test_basics_cmodel(self): - self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) - - def external_func_helper(self, collector: Callable, *args): - DLL = find_GSL() - if not DLL: - self.skipTest('Could not find amplgsl.dll library') - - m = pe.ConcreteModel() - m.x = pe.Var() - m.y = pe.Var() - m.z = pe.Var() - m.hypot = pe.ExternalFunction(library=DLL, function='gsl_hypot') - func = m.hypot(m.x, m.x * m.y) - m.E = pe.Expression(expr=2 * func) - m.y.fix(3) - e = m.z + m.x * m.E - named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) - self.assertEqual([m.E], named_exprs) - self.assertEqual([m.z, m.x, m.y], var_list) - self.assertEqual([m.y], fixed_vars) - self.assertEqual([func], external_funcs) - - def test_external(self): - self.external_func_helper(collect_vars_and_named_exprs) - - @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') - def test_external_cmodel(self): - self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) From 793fb38df3f98b04afde959f302e6e5185e33b6d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:57:34 -0600 Subject: [PATCH 0047/1178] Reverting test_branches file --- .github/workflows/test_branches.yml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index ff8b5901189..99d5f7fc1a8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -390,10 +390,10 @@ jobs: IPOPT_TAR=${DOWNLOAD_DIR}/ipopt.tar.gz if test ! -e $IPOPT_TAR; then echo "...downloading Ipopt" - # if test "${{matrix.TARGET}}" == osx; then - # echo "IDAES Ipopt not available on OSX" - # exit 0 - # fi + if test "${{matrix.TARGET}}" == osx; then + echo "IDAES Ipopt not available on OSX" + exit 0 + fi URL=https://github.com/IDAES/idaes-ext RELEASE=$(curl --max-time 150 --retry 8 \ -L -s -H 'Accept: application/json' ${URL}/releases/latest) @@ -401,11 +401,7 @@ jobs: URL=${URL}/releases/download/$VER if test "${{matrix.TARGET}}" == linux; then curl --max-time 150 --retry 8 \ - -L $URL/idaes-solvers-ubuntu2204-x86_64.tar.gz \ - > $IPOPT_TAR - elif test "${{matrix.TARGET}}" == osx; then - curl --max-time 150 --retry 8 \ - -L $URL/idaes-solvers-darwin-x86_64.tar.gz \ + -L $URL/idaes-solvers-ubuntu2004-x86_64.tar.gz \ > $IPOPT_TAR else curl --max-time 150 --retry 8 \ @@ -414,7 +410,7 @@ jobs: fi fi cd $IPOPT_DIR - tar -xz < $IPOPT_TAR + tar -xzi < $IPOPT_TAR echo "" echo "$IPOPT_DIR" ls -l $IPOPT_DIR @@ -602,7 +598,8 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo/contrib/appsi pyomo/solver --junitxml="TEST-pyomo.xml" + pyomo `pwd`/pyomo-model-libraries \ + `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 From 35e921ad611d0b8d9bbab1bffb488c6e17263e10 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 14:31:06 -0600 Subject: [PATCH 0048/1178] Add __init__ to test directory --- pyomo/solver/tests/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pyomo/solver/tests/__init__.py diff --git a/pyomo/solver/tests/__init__.py b/pyomo/solver/tests/__init__.py new file mode 100644 index 00000000000..9a63db93d6a --- /dev/null +++ b/pyomo/solver/tests/__init__.py @@ -0,0 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + From 3489dfcfa3a39d732596d17bcbdc58bb46233710 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:32:58 -0600 Subject: [PATCH 0049/1178] Update the results object --- .../contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 24 ++--- pyomo/contrib/appsi/solvers/cplex.py | 20 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 22 ++-- pyomo/contrib/appsi/solvers/highs.py | 14 +-- pyomo/contrib/appsi/solvers/ipopt.py | 22 ++-- .../solvers/tests/test_gurobi_persistent.py | 22 ++-- .../solvers/tests/test_persistent_solvers.py | 102 +++++++++--------- pyomo/solver/base.py | 65 +++++++---- pyomo/solver/config.py | 12 +-- pyomo/solver/tests/test_base.py | 4 +- 11 files changed, 166 insertions(+), 143 deletions(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index de5357776f4..79f1aa845b3 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -36,7 +36,7 @@ def main(plot=True, n_points=200): res.termination_condition == solver_base.TerminationCondition.convergenceCriteriaSatisfied ) - obj_values.append(res.best_feasible_objective) + obj_values.append(res.incumbent_objective) opt.load_vars([m.x]) x_values.append(m.x.value) timer.stop('p loop') diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 021ff76217d..9ae1ecba1f2 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -313,23 +313,23 @@ def _parse_soln(self): for v_id, (v, val) in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: - results.best_feasible_objective = obj_val + results.incumbent_objective = obj_val elif ( results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied ): if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: - results.best_feasible_objective = obj_val + results.incumbent_objective = obj_val elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) return results @@ -406,24 +406,24 @@ def _check_and_escape_options(): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.best_feasible_objective = None + results.incumbent_objective = None else: timer.start('parse solution') results = self._parse_soln() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.best_feasible_objective = None - results.best_objective_bound = None + results.incumbent_objective = None + results.objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -434,7 +434,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.best_feasible_objective is None + or self._last_results_object.incumbent_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 759bd7ff9d5..bab6afd7375 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -299,33 +299,33 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): results.termination_condition = TerminationCondition.unknown if self._writer.get_active_objective() is None: - results.best_feasible_objective = None - results.best_objective_bound = None + results.incumbent_objective = None + results.objective_bound = None else: if cpxprob.solution.get_solution_type() != cpxprob.solution.type.none: if ( cpxprob.variables.get_num_binary() + cpxprob.variables.get_num_integer() ) == 0: - results.best_feasible_objective = ( + results.incumbent_objective = ( cpxprob.solution.get_objective_value() ) - results.best_objective_bound = ( + results.objective_bound = ( cpxprob.solution.get_objective_value() ) else: - results.best_feasible_objective = ( + results.incumbent_objective = ( cpxprob.solution.get_objective_value() ) - results.best_objective_bound = ( + results.objective_bound = ( cpxprob.solution.MIP.get_best_objective() ) else: - results.best_feasible_objective = None + results.incumbent_objective = None if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: @@ -333,7 +333,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'A feasible solution was not found, so no solution can be loades. ' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) else: if ( diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index c2db835922d..8691151f475 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -899,25 +899,25 @@ def _postsolve(self, timer: HierarchicalTimer): else: results.termination_condition = TerminationCondition.unknown - results.best_feasible_objective = None - results.best_objective_bound = None + results.incumbent_objective = None + results.objective_bound = None if self._objective is not None: try: - results.best_feasible_objective = gprob.ObjVal + results.incumbent_objective = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): - results.best_feasible_objective = None + results.incumbent_objective = None try: - results.best_objective_bound = gprob.ObjBound + results.objective_bound = gprob.ObjBound except (gurobipy.GurobiError, AttributeError): if self._objective.sense == minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf - if results.best_feasible_objective is not None and not math.isfinite( - results.best_feasible_objective + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective ): - results.best_feasible_objective = None + results.incumbent_objective = None timer.start('load solution') if config.load_solution: @@ -938,7 +938,7 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3b7c92ed9e8..7b973a297f6 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -660,23 +660,23 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) timer.stop('load solution') info = highs.getInfo() - results.best_objective_bound = None - results.best_feasible_objective = None + results.objective_bound = None + results.incumbent_objective = None if self._objective is not None: if has_feasible_solution: - results.best_feasible_objective = info.objective_function_value + results.incumbent_objective = info.objective_function_value if info.mip_node_count == -1: if has_feasible_solution: - results.best_objective_bound = info.objective_function_value + results.objective_bound = info.objective_function_value else: - results.best_objective_bound = None + results.objective_bound = None else: - results.best_objective_bound = info.mip_dual_bound + results.objective_bound = info.mip_dual_bound return results diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 6c4b7601d2c..0249d97258f 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -390,9 +390,9 @@ def _parse_sol(self): for v, val in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: - results.best_feasible_objective = value( + results.incumbent_objective = value( self._writer.get_active_objective().expr ) elif ( @@ -400,7 +400,7 @@ def _parse_sol(self): == TerminationCondition.convergenceCriteriaSatisfied ): if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: obj_expr_evaluated = replace_expressions( self._writer.get_active_objective().expr, @@ -410,13 +410,13 @@ def _parse_sol(self): descend_into_named_expressions=True, remove_named_expressions=True, ) - results.best_feasible_objective = value(obj_expr_evaluated) + results.incumbent_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) return results @@ -480,23 +480,23 @@ def _apply_solver(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.best_feasible_objective = None + results.incumbent_objective = None else: timer.start('parse solution') results = self._parse_sol() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.best_objective_bound = None + results.objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -507,7 +507,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.best_feasible_objective is None + or self._last_results_object.incumbent_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 9fdce87b8de..7e1d3e37af6 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -155,12 +155,12 @@ def test_lp(self): x, y = self.get_solution() opt = Gurobi() res = opt.solve(self.m) - self.assertAlmostEqual(x + y, res.best_feasible_objective) - self.assertAlmostEqual(x + y, res.best_objective_bound) + self.assertAlmostEqual(x + y, res.incumbent_objective) + self.assertAlmostEqual(x + y, res.objective_bound) self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertTrue(res.best_feasible_objective is not None) + self.assertTrue(res.incumbent_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) @@ -196,11 +196,11 @@ def test_nonconvex_qcp_objective_bound_1(self): opt.gurobi_options['BestBdStop'] = -8 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.best_feasible_objective, None) - self.assertAlmostEqual(res.best_objective_bound, -8) + self.assertEqual(res.incumbent_objective, None) + self.assertAlmostEqual(res.objective_bound, -8) def test_nonconvex_qcp_objective_bound_2(self): - # the goal of this test is to ensure we can best_objective_bound properly + # the goal of this test is to ensure we can objective_bound properly # for nonconvex but continuous problems when the solver terminates with a nonzero gap # # This is a fragile test because it could fail if Gurobi's algorithms change @@ -214,8 +214,8 @@ def test_nonconvex_qcp_objective_bound_2(self): opt.gurobi_options['nonconvex'] = 2 opt.gurobi_options['MIPGap'] = 0.5 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -4) - self.assertAlmostEqual(res.best_objective_bound, -6) + self.assertAlmostEqual(res.incumbent_objective, -4) + self.assertAlmostEqual(res.objective_bound, -6) def test_range_constraints(self): m = pe.ConcreteModel() @@ -282,7 +282,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.best_feasible_objective, + res.incumbent_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -292,7 +292,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.best_feasible_objective, + res.incumbent_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -467,7 +467,7 @@ def test_zero_time_limit(self): # what we are trying to test. Unfortunately, I'm # not sure of a good way to guarantee that if num_solutions == 0: - self.assertIsNone(res.best_feasible_objective) + self.assertIsNone(res.incumbent_objective) class TestManualModel(unittest.TestCase): diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index bf92244ec36..352f93b7ad1 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -249,8 +249,8 @@ def test_param_changes( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -290,8 +290,8 @@ def test_immutable_param( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -327,8 +327,8 @@ def test_equality( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @@ -369,8 +369,8 @@ def test_linear_expression( TerminationCondition.convergenceCriteriaSatisfied, ) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_no_objective( @@ -403,8 +403,8 @@ def test_no_objective( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertEqual(res.best_feasible_objective, None) - self.assertEqual(res.best_objective_bound, None) + self.assertEqual(res.incumbent_objective, None) + self.assertEqual(res.objective_bound, None) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @@ -434,8 +434,8 @@ def test_add_remove_cons( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -447,8 +447,8 @@ def test_add_remove_cons( ) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -461,8 +461,8 @@ def test_add_remove_cons( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -502,7 +502,7 @@ def test_results_infeasible( self.assertIn(res.termination_condition, acceptable_termination_conditions) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) - self.assertTrue(res.best_feasible_objective is None) + self.assertTrue(res.incumbent_objective is None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' @@ -789,16 +789,16 @@ def test_mutable_param_with_range( if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) - self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound <= m.y.value + 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) - self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound >= m.y.value - 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -1077,13 +1077,13 @@ def test_objective_changes( m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) m.obj = pe.Objective(expr=m.y) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.obj = pe.Objective(expr=2 * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 2) + self.assertAlmostEqual(res.incumbent_objective, 2) m.obj.expr = 3 * m.y res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 3) + self.assertAlmostEqual(res.incumbent_objective, 3) m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) @@ -1099,30 +1099,30 @@ def test_objective_changes( m.obj = pe.Objective(expr=m.x * m.y) m.x.fix(2) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 6, 6) + self.assertAlmostEqual(res.incumbent_objective, 6, 6) m.x.fix(3) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 12, 6) + self.assertAlmostEqual(res.incumbent_objective, 12, 6) m.x.unfix() m.y.fix(2) m.x.setlb(-3) m.x.setub(5) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -2, 6) + self.assertAlmostEqual(res.incumbent_objective, -2, 6) m.y.unfix() m.x.setlb(None) m.x.setub(None) m.e = pe.Expression(expr=2) m.obj = pe.Objective(expr=m.e * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 2) + self.assertAlmostEqual(res.incumbent_objective, 2) m.e.expr = 3 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 3) + self.assertAlmostEqual(res.incumbent_objective, 3) opt.update_config.check_for_new_objective = False m.e.expr = 4 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 4) + self.assertAlmostEqual(res.incumbent_objective, 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_domain( @@ -1135,20 +1135,20 @@ def test_domain( m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.x.setlb(-1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.setlb(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.x.setlb(-1) m.x.domain = pe.Reals res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -1) + self.assertAlmostEqual(res.incumbent_objective, -1) m.x.domain = pe.NonNegativeReals res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_domain_with_integers( @@ -1161,20 +1161,20 @@ def test_domain_with_integers( m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.x.setlb(-5.5) m.x.domain = pe.Integers res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -5) + self.assertAlmostEqual(res.incumbent_objective, -5) m.x.domain = pe.Binary res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_binaries( @@ -1190,19 +1190,19 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( @@ -1226,7 +1226,7 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -1250,7 +1250,7 @@ def test_variables_elsewhere( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) @@ -1259,7 +1259,7 @@ def test_variables_elsewhere( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 2) + self.assertAlmostEqual(res.incumbent_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @@ -1286,7 +1286,7 @@ def test_variables_elsewhere2( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) @@ -1298,7 +1298,7 @@ def test_variables_elsewhere2( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) @@ -1324,14 +1324,14 @@ def test_bug_1( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.p.value = 1 res = opt.solve(m) self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 3) + self.assertAlmostEqual(res.incumbent_objective, 3) @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index f0a07d0aca3..efce7b09f54 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -11,12 +11,14 @@ import abc import enum +from datetime import datetime from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeInt, In, NonNegativeFloat from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -106,37 +108,62 @@ class SolutionStatus(enum.IntEnum): optimal = 30 -class Results: +class Results(ConfigDict): """ Attributes ---------- termination_condition: TerminationCondition The reason the solver exited. This is a member of the TerminationCondition enum. - best_feasible_objective: float + incumbent_objective: float If a feasible solution was found, this is the objective value of the best solution found. If no feasible solution was found, this is None. - best_objective_bound: float + objective_bound: float The best objective bound found. For minimization problems, this is the lower bound. For maximization problems, this is the upper bound. For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) """ - def __init__(self): - self.solution_loader: SolutionLoaderBase = SolutionLoader( - None, None, None, None + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, ) - self.termination_condition: TerminationCondition = TerminationCondition.unknown - self.best_feasible_objective: Optional[float] = None - self.best_objective_bound: Optional[float] = None + + self.declare('solution_loader', ConfigValue(domain=In(SolutionLoaderBase), default=SolutionLoader( + None, None, None, None + ))) + self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) + self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) + self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) + self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) + self.declare('solver_name', ConfigValue(domain=str)) + self.declare('solver_version', ConfigValue(domain=tuple)) + self.declare('termination_message', ConfigValue(domain=str)) + self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) + self.declare('timing_info', ConfigDict()) + self.timing_info.declare('start', ConfigValue=In(datetime)) + self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) + self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) + self.declare('extra_info', ConfigDict(implicit=True)) def __str__(self): s = '' s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' - s += 'best_objective_bound: ' + str(self.best_objective_bound) + s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' + s += 'objective_bound: ' + str(self.objective_bound) return s @@ -496,17 +523,17 @@ def solve( legacy_results.problem.sense = obj.sense if obj.sense == minimize: - legacy_results.problem.lower_bound = results.best_objective_bound - legacy_results.problem.upper_bound = results.best_feasible_objective + legacy_results.problem.lower_bound = results.objective_bound + legacy_results.problem.upper_bound = results.incumbent_objective else: - legacy_results.problem.upper_bound = results.best_objective_bound - legacy_results.problem.lower_bound = results.best_feasible_objective + legacy_results.problem.upper_bound = results.objective_bound + legacy_results.problem.lower_bound = results.incumbent_objective if ( - results.best_feasible_objective is not None - and results.best_objective_bound is not None + results.incumbent_objective is not None + and results.objective_bound is not None ): legacy_soln.gap = abs( - results.best_feasible_objective - results.best_objective_bound + results.incumbent_objective - results.objective_bound ) else: legacy_soln.gap = None @@ -530,7 +557,7 @@ def solve( if hasattr(model, 'rc') and model.rc.import_enabled(): for v, val in results.solution_loader.get_reduced_costs().items(): model.rc[v] = val - elif results.best_feasible_objective is not None: + elif results.incumbent_objective is not None: delete_legacy_soln = False for v, val in results.solution_loader.get_primals().items(): legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index f446dc714db..32f6e1d5da0 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -56,19 +56,15 @@ def __init__( visibility=visibility, ) - self.declare('tee', ConfigValue(domain=bool)) - self.declare('load_solution', ConfigValue(domain=bool)) - self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) - self.declare('report_timing', ConfigValue(domain=bool)) + self.declare('tee', ConfigValue(domain=bool, default=False)) + self.declare('load_solution', ConfigValue(domain=bool, default=True)) + self.declare('symbolic_solver_labels', ConfigValue(domain=bool, default=False)) + self.declare('report_timing', ConfigValue(domain=bool, default=False)) self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) - self.tee: bool = False - self.load_solution: bool = True - self.symbolic_solver_labels: bool = False - self.report_timing: bool = False class MIPInterfaceConfig(InterfaceConfig): diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 355941a1eb1..41b768520c1 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -139,8 +139,8 @@ def test_persistent_solver_base(self): class TestResults(unittest.TestCase): def test_uninitialized(self): res = base.Results() - self.assertIsNone(res.best_feasible_objective) - self.assertIsNone(res.best_objective_bound) + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) with self.assertRaisesRegex( From 146fa0a10e70995ca3b82b29897a5efcc2e26fad Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:39:34 -0600 Subject: [PATCH 0050/1178] Back to only running appsi/solver test --- .github/workflows/test_branches.yml | 3 +-- pyomo/solver/base.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 99d5f7fc1a8..a944bbdd645 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -598,8 +598,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo `pwd`/pyomo-model-libraries \ - `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" + pyomo/contrib/appsi pyomo/solver --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index efce7b09f54..2b5f81bef82 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -147,8 +147,8 @@ def __init__( ))) self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) - self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) - self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) + self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=NonNegativeFloat)) + self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=NonNegativeFloat)) self.declare('solver_name', ConfigValue(domain=str)) self.declare('solver_version', ConfigValue(domain=tuple)) self.declare('termination_message', ConfigValue(domain=str)) From e2d0592ec5f9714082959870eca482946f998c5d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:50:55 -0600 Subject: [PATCH 0051/1178] Remove domain specification --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2b5f81bef82..e39e47264ad 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -142,7 +142,7 @@ def __init__( visibility=visibility, ) - self.declare('solution_loader', ConfigValue(domain=In(SolutionLoaderBase), default=SolutionLoader( + self.declare('solution_loader', ConfigValue(default=SolutionLoader( None, None, None, None ))) self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) From b40ff29f023852963c50f27886068b5d07baf47d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:57:40 -0600 Subject: [PATCH 0052/1178] Fix domain typo --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index e39e47264ad..1872011bcd9 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -154,7 +154,7 @@ def __init__( self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) - self.timing_info.declare('start', ConfigValue=In(datetime)) + self.timing_info.declare('start', ConfigValue(domain=In(datetime))) self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) self.declare('extra_info', ConfigDict(implicit=True)) From 050ceb661d48ab3d2ce825b63a2a600745e8b217 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:15:10 -0600 Subject: [PATCH 0053/1178] Allow negative floats --- pyomo/solver/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 1872011bcd9..0c77839e358 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -147,8 +147,8 @@ def __init__( ))) self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) - self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=NonNegativeFloat)) - self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=NonNegativeFloat)) + self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) + self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) self.declare('solver_name', ConfigValue(domain=str)) self.declare('solver_version', ConfigValue(domain=tuple)) self.declare('termination_message', ConfigValue(domain=str)) From ceb858a1bf17cc0a0e2935809b78f2fc5fb4e90c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:16:10 -0600 Subject: [PATCH 0054/1178] Remove type checking for start_time --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 0c77839e358..846431ed918 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -154,7 +154,7 @@ def __init__( self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) - self.timing_info.declare('start', ConfigValue(domain=In(datetime))) + self.timing_info.declare('start_time') self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) self.declare('extra_info', ConfigDict(implicit=True)) From cc0ad9b33dcfb1d3f7db2db3791301778bf5c125 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:25:11 -0600 Subject: [PATCH 0055/1178] Add empty config value to start_time --- pyomo/contrib/appsi/solvers/cplex.py | 16 +++------- pyomo/solver/base.py | 45 ++++++++++++++++++++-------- pyomo/solver/tests/__init__.py | 2 -- pyomo/solver/tests/test_base.py | 38 ++++++++++++----------- pyomo/solver/tests/test_config.py | 2 +- pyomo/solver/tests/test_solution.py | 5 +++- 6 files changed, 61 insertions(+), 47 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index bab6afd7375..ac9eaab471f 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -307,19 +307,11 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): cpxprob.variables.get_num_binary() + cpxprob.variables.get_num_integer() ) == 0: - results.incumbent_objective = ( - cpxprob.solution.get_objective_value() - ) - results.objective_bound = ( - cpxprob.solution.get_objective_value() - ) + results.incumbent_objective = cpxprob.solution.get_objective_value() + results.objective_bound = cpxprob.solution.get_objective_value() else: - results.incumbent_objective = ( - cpxprob.solution.get_objective_value() - ) - results.objective_bound = ( - cpxprob.solution.MIP.get_best_objective() - ) + results.incumbent_objective = cpxprob.solution.get_objective_value() + results.objective_bound = cpxprob.solution.MIP.get_best_objective() else: results.incumbent_objective = None if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 846431ed918..2e34747884d 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -18,7 +18,13 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeInt, In, NonNegativeFloat +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeInt, + In, + NonNegativeFloat, +) from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -142,21 +148,36 @@ def __init__( visibility=visibility, ) - self.declare('solution_loader', ConfigValue(default=SolutionLoader( - None, None, None, None - ))) - self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) - self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) - self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) - self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) + self.declare( + 'solution_loader', + ConfigValue(default=SolutionLoader(None, None, None, None)), + ) + self.declare( + 'termination_condition', + ConfigValue( + domain=In(TerminationCondition), default=TerminationCondition.unknown + ), + ) + self.declare( + 'solution_status', + ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), + ) + self.incumbent_objective: Optional[float] = self.declare( + 'incumbent_objective', ConfigValue(domain=float) + ) + self.objective_bound: Optional[float] = self.declare( + 'objective_bound', ConfigValue(domain=float) + ) self.declare('solver_name', ConfigValue(domain=str)) self.declare('solver_version', ConfigValue(domain=tuple)) self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) - self.timing_info.declare('start_time') + self.timing_info.declare('start_time', ConfigValue()) self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) - self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) + self.timing_info.declare( + 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) + ) self.declare('extra_info', ConfigDict(implicit=True)) def __str__(self): @@ -532,9 +553,7 @@ def solve( results.incumbent_objective is not None and results.objective_bound is not None ): - legacy_soln.gap = abs( - results.incumbent_objective - results.objective_bound - ) + legacy_soln.gap = abs(results.incumbent_objective - results.objective_bound) else: legacy_soln.gap = None diff --git a/pyomo/solver/tests/__init__.py b/pyomo/solver/tests/__init__.py index 9a63db93d6a..d93cfd77b3c 100644 --- a/pyomo/solver/tests/__init__.py +++ b/pyomo/solver/tests/__init__.py @@ -8,5 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - - diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 41b768520c1..34d5c47d11c 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -89,24 +89,26 @@ def test_solver_availability(self): class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): - expected_list = ['remove_params', - 'version', - 'config', - 'update_variables', - 'remove_variables', - 'add_constraints', - 'get_primals', - 'set_instance', - 'set_objective', - 'update_params', - 'remove_block', - 'add_block', - 'available', - 'update_config', - 'add_params', - 'remove_constraints', - 'add_variables', - 'solve'] + expected_list = [ + 'remove_params', + 'version', + 'config', + 'update_variables', + 'remove_variables', + 'add_constraints', + 'get_primals', + 'set_instance', + 'set_objective', + 'update_params', + 'remove_block', + 'add_block', + 'available', + 'update_config', + 'add_params', + 'remove_constraints', + 'add_variables', + 'solve', + ] member_list = list(base.PersistentSolverBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index 378facb58d2..49d26513e2e 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -12,8 +12,8 @@ from pyomo.common import unittest from pyomo.solver.config import InterfaceConfig, MIPInterfaceConfig -class TestInterfaceConfig(unittest.TestCase): +class TestInterfaceConfig(unittest.TestCase): def test_interface_default_instantiation(self): config = InterfaceConfig() self.assertEqual(config._description, None) diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py index c4c2f790b55..f4c33a60c84 100644 --- a/pyomo/solver/tests/test_solution.py +++ b/pyomo/solver/tests/test_solution.py @@ -12,13 +12,16 @@ from pyomo.common import unittest from pyomo.solver import solution + class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = ['get_primals'] member_list = list(solution.SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) - @unittest.mock.patch.multiple(solution.SolutionLoaderBase, __abstractmethods__=set()) + @unittest.mock.patch.multiple( + solution.SolutionLoaderBase, __abstractmethods__=set() + ) def test_solution_loader_base(self): self.instance = solution.SolutionLoaderBase() self.assertEqual(self.instance.get_primals(), None) From 74a459e6ca0cbfd76099540710e1a3c19ef0772e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:34:15 -0600 Subject: [PATCH 0056/1178] Change result attribute in HiGHS to match new standard --- pyomo/contrib/appsi/solvers/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 7b973a297f6..f8003599387 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -63,7 +63,7 @@ def __init__( class HighsResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None + self.timing_info.wall_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) From 154b43803a7e4eef4c88891cf0c0cfd7702852f3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:41:18 -0600 Subject: [PATCH 0057/1178] Replace all other instances of wallclock_time --- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index ac9eaab471f..34ad88aeb7a 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -58,7 +58,7 @@ def __init__( class CplexResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None + self.timing_info.wall_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) @@ -278,7 +278,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): cpxprob = self._cplex_model results = CplexResults(solver=self) - results.wallclock_time = solve_time + results.timing_info.wall_time = solve_time status = cpxprob.solution.get_status() if status in [1, 101, 102]: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 8691151f475..cd116bcbefa 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -93,7 +93,7 @@ def get_primals(self, vars_to_load=None, solution_number=0): class GurobiResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None + self.timing_info.wall_time = None self.solution_loader = GurobiSolutionLoader(solver=solver) @@ -864,7 +864,7 @@ def _postsolve(self, timer: HierarchicalTimer): status = gprob.Status results = GurobiResults(self) - results.wallclock_time = gprob.Runtime + results.timing_info.wall_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index f8003599387..a29dc2a597f 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -587,7 +587,7 @@ def _postsolve(self, timer: HierarchicalTimer): status = highs.getModelStatus() results = HighsResults(self) - results.wallclock_time = highs.getRunTime() + results.timing_info.wall_time = highs.getRunTime() if status == highspy.HighsModelStatus.kNotset: results.termination_condition = TerminationCondition.unknown From b6f1e2a63aa2c8ad235afb57f73a9da9aa6a36a6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:58:39 -0600 Subject: [PATCH 0058/1178] Update unit tests for Results object --- pyomo/solver/tests/test_base.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 34d5c47d11c..0e0780fd6fe 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest +from pyomo.common.config import ConfigDict from pyomo.solver import base import pyomo.environ as pe from pyomo.core.base.var import ScalarVar @@ -130,20 +131,51 @@ def test_persistent_solver_base(self): self.assertEqual(self.instance.set_objective(None), None) self.assertEqual(self.instance.update_variables(None), None) self.assertEqual(self.instance.update_params(), None) + with self.assertRaises(NotImplementedError): self.instance.get_duals() + with self.assertRaises(NotImplementedError): self.instance.get_slacks() + with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() class TestResults(unittest.TestCase): + def test_declared_items(self): + res = base.Results() + expected_declared = { + 'extra_info', + 'incumbent_objective', + 'iteration_count', + 'objective_bound', + 'solution_loader', + 'solution_status', + 'solver_name', + 'solver_version', + 'termination_condition', + 'termination_message', + 'timing_info', + } + actual_declared = res._declared + self.assertEqual(expected_declared, actual_declared) + def test_uninitialized(self): res = base.Results() self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) + self.assertEqual(res.solution_status, base.SolutionStatus.noSolution) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.termination_message) + self.assertIsNone(res.iteration_count) + self.assertIsInstance(res.timing_info, ConfigDict) + self.assertIsInstance(res.extra_info, ConfigDict) + self.assertIsNone(res.timing_info.start_time) + self.assertIsNone(res.timing_info.wall_time) + self.assertIsNone(res.timing_info.solver_wall_time) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' From 072ac658c18d71aa7f68331d790db11e3c892a71 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:20:37 -0600 Subject: [PATCH 0059/1178] Update solution status map --- pyomo/solver/base.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2e34747884d..fa52883f3e2 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -43,7 +43,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.config import UpdateConfig -from pyomo.solver.solution import SolutionLoader, SolutionLoaderBase +from pyomo.solver.solution import SolutionLoader from pyomo.solver.util import get_objective @@ -173,6 +173,7 @@ def __init__( self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) + # TODO: Set up type checking for start_time self.timing_info.declare('start_time', ConfigValue()) self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) self.timing_info.declare( @@ -183,6 +184,7 @@ def __init__( def __str__(self): s = '' s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'solution_status: ' + str(self.solution_status) + '\n' s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' s += 'objective_bound: ' + str(self.objective_bound) return s @@ -472,19 +474,18 @@ def update_params(self): legacy_solution_status_map = { - TerminationCondition.unknown: LegacySolutionStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, - TerminationCondition.unbounded: LegacySolutionStatus.unbounded, - TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, - TerminationCondition.error: LegacySolutionStatus.error, - TerminationCondition.interrupted: LegacySolutionStatus.error, - TerminationCondition.licensingProblems: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.unknown, + SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, + SolutionStatus.noSolution: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.other, + SolutionStatus.noSolution: LegacySolutionStatus.unsure, + SolutionStatus.noSolution: LegacySolutionStatus.unbounded, + SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.optimal, + SolutionStatus.infeasible: LegacySolutionStatus.infeasible, + SolutionStatus.feasible: LegacySolutionStatus.feasible, + SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, } From eeceb8667a9d01c49c9c7f3076a847a63829e677 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:37:32 -0600 Subject: [PATCH 0060/1178] Refactor Results to be in its own file --- pyomo/contrib/appsi/solvers/cbc.py | 3 +- pyomo/contrib/appsi/solvers/cplex.py | 3 +- pyomo/contrib/appsi/solvers/gurobi.py | 3 +- pyomo/contrib/appsi/solvers/highs.py | 3 +- pyomo/contrib/appsi/solvers/ipopt.py | 3 +- .../solvers/tests/test_persistent_solvers.py | 3 +- pyomo/solver/base.py | 213 +---------------- pyomo/solver/results.py | 225 ++++++++++++++++++ pyomo/solver/tests/test_base.py | 167 ------------- pyomo/solver/tests/test_results.py | 180 ++++++++++++++ 10 files changed, 420 insertions(+), 383 deletions(-) create mode 100644 pyomo/solver/results.py create mode 100644 pyomo/solver/tests/test_results.py diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 9ae1ecba1f2..c2686475b15 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -22,8 +22,9 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import InterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 34ad88aeb7a..86d50f1b82a 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -19,8 +19,9 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index cd116bcbefa..1f295dfcb49 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -22,8 +22,9 @@ from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a29dc2a597f..b5b2cc3b694 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,8 +20,9 @@ from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 0249d97258f..b16ca4dc792 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -26,8 +26,9 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import InterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 352f93b7ad1..5ef6dd7ba50 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,7 +4,8 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase +from pyomo.solver.results import TerminationCondition, Results from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index fa52883f3e2..2d5bde41329 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -11,183 +11,25 @@ import abc import enum -from datetime import datetime from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - NonNegativeInt, - In, - NonNegativeFloat, -) from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory import os from pyomo.opt.results.results_ import SolverResults as LegacySolverResults -from pyomo.opt.results.solution import ( - Solution as LegacySolution, - SolutionStatus as LegacySolutionStatus, -) -from pyomo.opt.results.solver import ( - TerminationCondition as LegacyTerminationCondition, - SolverStatus as LegacySolverStatus, -) +from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.config import UpdateConfig -from pyomo.solver.solution import SolutionLoader from pyomo.solver.util import get_objective - - -class TerminationCondition(enum.Enum): - """ - An enumeration for checking the termination condition of solvers - """ - - """unknown serves as both a default value, and it is used when no other enum member makes sense""" - unknown = 42 - - """The solver exited because the convergence criteria were satisfied""" - convergenceCriteriaSatisfied = 0 - - """The solver exited due to a time limit""" - maxTimeLimit = 1 - - """The solver exited due to an iteration limit""" - iterationLimit = 2 - - """The solver exited due to an objective limit""" - objectiveLimit = 3 - - """The solver exited due to a minimum step length""" - minStepLength = 4 - - """The solver exited because the problem is unbounded""" - unbounded = 5 - - """The solver exited because the problem is proven infeasible""" - provenInfeasible = 6 - - """The solver exited because the problem was found to be locally infeasible""" - locallyInfeasible = 7 - - """The solver exited because the problem is either infeasible or unbounded""" - infeasibleOrUnbounded = 8 - - """The solver exited due to an error""" - error = 9 - - """The solver exited because it was interrupted""" - interrupted = 10 - - """The solver exited due to licensing problems""" - licensingProblems = 11 - - -class SolutionStatus(enum.IntEnum): - """ - An enumeration for interpreting the result of a termination. This describes the designated - status by the solver to be loaded back into the model. - - For now, we are choosing to use IntEnum such that return values are numerically - assigned in increasing order. - """ - - """No (single) solution found; possible that a population of solutions was returned""" - noSolution = 0 - - """Solution point does not satisfy some domains and/or constraints""" - infeasible = 10 - - """Feasible solution identified""" - feasible = 20 - - """Optimal solution identified""" - optimal = 30 - - -class Results(ConfigDict): - """ - Attributes - ---------- - termination_condition: TerminationCondition - The reason the solver exited. This is a member of the - TerminationCondition enum. - incumbent_objective: float - If a feasible solution was found, this is the objective value of - the best solution found. If no feasible solution was found, this is - None. - objective_bound: float - The best objective bound found. For minimization problems, this is - the lower bound. For maximization problems, this is the upper bound. - For solvers that do not provide an objective bound, this should be -inf - (minimization) or inf (maximization) - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare( - 'solution_loader', - ConfigValue(default=SolutionLoader(None, None, None, None)), - ) - self.declare( - 'termination_condition', - ConfigValue( - domain=In(TerminationCondition), default=TerminationCondition.unknown - ), - ) - self.declare( - 'solution_status', - ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), - ) - self.incumbent_objective: Optional[float] = self.declare( - 'incumbent_objective', ConfigValue(domain=float) - ) - self.objective_bound: Optional[float] = self.declare( - 'objective_bound', ConfigValue(domain=float) - ) - self.declare('solver_name', ConfigValue(domain=str)) - self.declare('solver_version', ConfigValue(domain=tuple)) - self.declare('termination_message', ConfigValue(domain=str)) - self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) - self.declare('timing_info', ConfigDict()) - # TODO: Set up type checking for start_time - self.timing_info.declare('start_time', ConfigValue()) - self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) - self.timing_info.declare( - 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) - ) - self.declare('extra_info', ConfigDict(implicit=True)) - - def __str__(self): - s = '' - s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'solution_status: ' + str(self.solution_status) + '\n' - s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' - s += 'objective_bound: ' + str(self.objective_bound) - return s +from pyomo.solver.results import Results, legacy_solver_status_map, legacy_termination_condition_map, legacy_solution_status_map class SolverBase(abc.ABC): @@ -437,56 +279,7 @@ def update_params(self): pass -# Everything below here preserves backwards compatibility - -legacy_termination_condition_map = { - TerminationCondition.unknown: LegacyTerminationCondition.unknown, - TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, - TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, - TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, - TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, - TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, - TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, - TerminationCondition.error: LegacyTerminationCondition.error, - TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, - TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, -} - - -legacy_solver_status_map = { - TerminationCondition.unknown: LegacySolverStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, - TerminationCondition.iterationLimit: LegacySolverStatus.aborted, - TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, - TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, - TerminationCondition.unbounded: LegacySolverStatus.error, - TerminationCondition.provenInfeasible: LegacySolverStatus.error, - TerminationCondition.locallyInfeasible: LegacySolverStatus.error, - TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, - TerminationCondition.error: LegacySolverStatus.error, - TerminationCondition.interrupted: LegacySolverStatus.aborted, - TerminationCondition.licensingProblems: LegacySolverStatus.error, -} - - -legacy_solution_status_map = { - SolutionStatus.noSolution: LegacySolutionStatus.unknown, - SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, - SolutionStatus.noSolution: LegacySolutionStatus.error, - SolutionStatus.noSolution: LegacySolutionStatus.other, - SolutionStatus.noSolution: LegacySolutionStatus.unsure, - SolutionStatus.noSolution: LegacySolutionStatus.unbounded, - SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, - SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, - SolutionStatus.optimal: LegacySolutionStatus.optimal, - SolutionStatus.infeasible: LegacySolutionStatus.infeasible, - SolutionStatus.feasible: LegacySolutionStatus.feasible, - SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, -} + class LegacySolverInterface: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py new file mode 100644 index 00000000000..0b6fdcafbc4 --- /dev/null +++ b/pyomo/solver/results.py @@ -0,0 +1,225 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum +from typing import Optional +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeInt, + In, + NonNegativeFloat, +) +from pyomo.solver.solution import SolutionLoader +from pyomo.opt.results.solution import ( + SolutionStatus as LegacySolutionStatus, +) +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + unknown = 42 + + """The solver exited because the convergence criteria were satisfied""" + convergenceCriteriaSatisfied = 0 + + """The solver exited due to a time limit""" + maxTimeLimit = 1 + + """The solver exited due to an iteration limit""" + iterationLimit = 2 + + """The solver exited due to an objective limit""" + objectiveLimit = 3 + + """The solver exited due to a minimum step length""" + minStepLength = 4 + + """The solver exited because the problem is unbounded""" + unbounded = 5 + + """The solver exited because the problem is proven infeasible""" + provenInfeasible = 6 + + """The solver exited because the problem was found to be locally infeasible""" + locallyInfeasible = 7 + + """The solver exited because the problem is either infeasible or unbounded""" + infeasibleOrUnbounded = 8 + + """The solver exited due to an error""" + error = 9 + + """The solver exited because it was interrupted""" + interrupted = 10 + + """The solver exited due to licensing problems""" + licensingProblems = 11 + + +class SolutionStatus(enum.IntEnum): + """ + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + For now, we are choosing to use IntEnum such that return values are numerically + assigned in increasing order. + """ + + """No (single) solution found; possible that a population of solutions was returned""" + noSolution = 0 + + """Solution point does not satisfy some domains and/or constraints""" + infeasible = 10 + + """Feasible solution identified""" + feasible = 20 + + """Optimal solution identified""" + optimal = 30 + + +class Results(ConfigDict): + """ + Attributes + ---------- + termination_condition: TerminationCondition + The reason the solver exited. This is a member of the + TerminationCondition enum. + incumbent_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare( + 'solution_loader', + ConfigValue(default=SolutionLoader(None, None, None, None)), + ) + self.declare( + 'termination_condition', + ConfigValue( + domain=In(TerminationCondition), default=TerminationCondition.unknown + ), + ) + self.declare( + 'solution_status', + ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), + ) + self.incumbent_objective: Optional[float] = self.declare( + 'incumbent_objective', ConfigValue(domain=float) + ) + self.objective_bound: Optional[float] = self.declare( + 'objective_bound', ConfigValue(domain=float) + ) + self.declare('solver_name', ConfigValue(domain=str)) + self.declare('solver_version', ConfigValue(domain=tuple)) + self.declare('termination_message', ConfigValue(domain=str)) + self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) + self.declare('timing_info', ConfigDict()) + # TODO: Set up type checking for start_time + self.timing_info.declare('start_time', ConfigValue()) + self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) + self.timing_info.declare( + 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) + ) + self.declare('extra_info', ConfigDict(implicit=True)) + + def __str__(self): + s = '' + s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'solution_status: ' + str(self.solution_status) + '\n' + s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' + s += 'objective_bound: ' + str(self.objective_bound) + return s + + +# Everything below here preserves backwards compatibility + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + SolutionStatus.noSolution: LegacySolutionStatus.unknown, + SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, + SolutionStatus.noSolution: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.other, + SolutionStatus.noSolution: LegacySolutionStatus.unsure, + SolutionStatus.noSolution: LegacySolutionStatus.unbounded, + SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.optimal, + SolutionStatus.infeasible: LegacySolutionStatus.infeasible, + SolutionStatus.feasible: LegacySolutionStatus.feasible, + SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, +} + + diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 0e0780fd6fe..d8084e9b5b7 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -10,61 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.common.config import ConfigDict from pyomo.solver import base -import pyomo.environ as pe -from pyomo.core.base.var import ScalarVar - - -class TestTerminationCondition(unittest.TestCase): - def test_member_list(self): - member_list = base.TerminationCondition._member_names_ - expected_list = [ - 'unknown', - 'convergenceCriteriaSatisfied', - 'maxTimeLimit', - 'iterationLimit', - 'objectiveLimit', - 'minStepLength', - 'unbounded', - 'provenInfeasible', - 'locallyInfeasible', - 'infeasibleOrUnbounded', - 'error', - 'interrupted', - 'licensingProblems', - ] - self.assertEqual(member_list, expected_list) - - def test_codes(self): - self.assertEqual(base.TerminationCondition.unknown.value, 42) - self.assertEqual( - base.TerminationCondition.convergenceCriteriaSatisfied.value, 0 - ) - self.assertEqual(base.TerminationCondition.maxTimeLimit.value, 1) - self.assertEqual(base.TerminationCondition.iterationLimit.value, 2) - self.assertEqual(base.TerminationCondition.objectiveLimit.value, 3) - self.assertEqual(base.TerminationCondition.minStepLength.value, 4) - self.assertEqual(base.TerminationCondition.unbounded.value, 5) - self.assertEqual(base.TerminationCondition.provenInfeasible.value, 6) - self.assertEqual(base.TerminationCondition.locallyInfeasible.value, 7) - self.assertEqual(base.TerminationCondition.infeasibleOrUnbounded.value, 8) - self.assertEqual(base.TerminationCondition.error.value, 9) - self.assertEqual(base.TerminationCondition.interrupted.value, 10) - self.assertEqual(base.TerminationCondition.licensingProblems.value, 11) - - -class TestSolutionStatus(unittest.TestCase): - def test_member_list(self): - member_list = base.SolutionStatus._member_names_ - expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] - self.assertEqual(member_list, expected_list) - - def test_codes(self): - self.assertEqual(base.SolutionStatus.noSolution.value, 0) - self.assertEqual(base.SolutionStatus.infeasible.value, 10) - self.assertEqual(base.SolutionStatus.feasible.value, 20) - self.assertEqual(base.SolutionStatus.optimal.value, 30) class TestSolverBase(unittest.TestCase): @@ -140,116 +86,3 @@ def test_persistent_solver_base(self): with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() - - -class TestResults(unittest.TestCase): - def test_declared_items(self): - res = base.Results() - expected_declared = { - 'extra_info', - 'incumbent_objective', - 'iteration_count', - 'objective_bound', - 'solution_loader', - 'solution_status', - 'solver_name', - 'solver_version', - 'termination_condition', - 'termination_message', - 'timing_info', - } - actual_declared = res._declared - self.assertEqual(expected_declared, actual_declared) - - def test_uninitialized(self): - res = base.Results() - self.assertIsNone(res.incumbent_objective) - self.assertIsNone(res.objective_bound) - self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) - self.assertEqual(res.solution_status, base.SolutionStatus.noSolution) - self.assertIsNone(res.solver_name) - self.assertIsNone(res.solver_version) - self.assertIsNone(res.termination_message) - self.assertIsNone(res.iteration_count) - self.assertIsInstance(res.timing_info, ConfigDict) - self.assertIsInstance(res.extra_info, ConfigDict) - self.assertIsNone(res.timing_info.start_time) - self.assertIsNone(res.timing_info.wall_time) - self.assertIsNone(res.timing_info.solver_wall_time) - - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have a valid solution.*' - ): - res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid slacks.*' - ): - res.solution_loader.get_slacks() - - def test_results(self): - m = pe.ConcreteModel() - m.x = ScalarVar() - m.y = ScalarVar() - m.c1 = pe.Constraint(expr=m.x == 1) - m.c2 = pe.Constraint(expr=m.y == 2) - - primals = {} - primals[id(m.x)] = (m.x, 1) - primals[id(m.y)] = (m.y, 2) - duals = {} - duals[m.c1] = 3 - duals[m.c2] = 4 - rc = {} - rc[id(m.x)] = (m.x, 5) - rc[id(m.y)] = (m.y, 6) - slacks = {} - slacks[m.c1] = 7 - slacks[m.c2] = 8 - - res = base.Results() - res.solution_loader = base.SolutionLoader( - primals=primals, duals=duals, slacks=slacks, reduced_costs=rc - ) - - res.solution_loader.load_vars() - self.assertAlmostEqual(m.x.value, 1) - self.assertAlmostEqual(m.y.value, 2) - - m.x.value = None - m.y.value = None - - res.solution_loader.load_vars([m.y]) - self.assertIsNone(m.x.value) - self.assertAlmostEqual(m.y.value, 2) - - duals2 = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - duals2 = res.solution_loader.get_duals([m.c2]) - self.assertNotIn(m.c1, duals2) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - rc2 = res.solution_loader.get_reduced_costs() - self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - rc2 = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, rc2) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - slacks2 = res.solution_loader.get_slacks() - self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) - - slacks2 = res.solution_loader.get_slacks([m.c2]) - self.assertNotIn(m.c1, slacks2) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py new file mode 100644 index 00000000000..74c0f9f2256 --- /dev/null +++ b/pyomo/solver/tests/test_results.py @@ -0,0 +1,180 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.common.config import ConfigDict +from pyomo.solver import results +import pyomo.environ as pyo +from pyomo.core.base.var import ScalarVar + + +class TestTerminationCondition(unittest.TestCase): + def test_member_list(self): + member_list = results.TerminationCondition._member_names_ + expected_list = [ + 'unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems', + ] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(results.TerminationCondition.unknown.value, 42) + self.assertEqual( + results.TerminationCondition.convergenceCriteriaSatisfied.value, 0 + ) + self.assertEqual(results.TerminationCondition.maxTimeLimit.value, 1) + self.assertEqual(results.TerminationCondition.iterationLimit.value, 2) + self.assertEqual(results.TerminationCondition.objectiveLimit.value, 3) + self.assertEqual(results.TerminationCondition.minStepLength.value, 4) + self.assertEqual(results.TerminationCondition.unbounded.value, 5) + self.assertEqual(results.TerminationCondition.provenInfeasible.value, 6) + self.assertEqual(results.TerminationCondition.locallyInfeasible.value, 7) + self.assertEqual(results.TerminationCondition.infeasibleOrUnbounded.value, 8) + self.assertEqual(results.TerminationCondition.error.value, 9) + self.assertEqual(results.TerminationCondition.interrupted.value, 10) + self.assertEqual(results.TerminationCondition.licensingProblems.value, 11) + + +class TestSolutionStatus(unittest.TestCase): + def test_member_list(self): + member_list = results.SolutionStatus._member_names_ + expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(results.SolutionStatus.noSolution.value, 0) + self.assertEqual(results.SolutionStatus.infeasible.value, 10) + self.assertEqual(results.SolutionStatus.feasible.value, 20) + self.assertEqual(results.SolutionStatus.optimal.value, 30) + + +class TestResults(unittest.TestCase): + def test_declared_items(self): + res = results.Results() + expected_declared = { + 'extra_info', + 'incumbent_objective', + 'iteration_count', + 'objective_bound', + 'solution_loader', + 'solution_status', + 'solver_name', + 'solver_version', + 'termination_condition', + 'termination_message', + 'timing_info', + } + actual_declared = res._declared + self.assertEqual(expected_declared, actual_declared) + + def test_uninitialized(self): + res = results.Results() + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) + self.assertEqual(res.termination_condition, results.TerminationCondition.unknown) + self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.termination_message) + self.assertIsNone(res.iteration_count) + self.assertIsInstance(res.timing_info, ConfigDict) + self.assertIsInstance(res.extra_info, ConfigDict) + self.assertIsNone(res.timing_info.start_time) + self.assertIsNone(res.timing_info.wall_time) + self.assertIsNone(res.timing_info.solver_wall_time) + + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid slacks.*' + ): + res.solution_loader.get_slacks() + + def test_results(self): + m = pyo.ConcreteModel() + m.x = ScalarVar() + m.y = ScalarVar() + m.c1 = pyo.Constraint(expr=m.x == 1) + m.c2 = pyo.Constraint(expr=m.y == 2) + + primals = {} + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = {} + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = {} + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + slacks = {} + slacks[m.c1] = 7 + slacks[m.c2] = 8 + + res = results.Results() + res.solution_loader = results.SolutionLoader( + primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + slacks2 = res.solution_loader.get_slacks() + self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) + + slacks2 = res.solution_loader.get_slacks([m.c2]) + self.assertNotIn(m.c1, slacks2) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) From f0a942cba9e33b1272e3f8a5296ab4a439ce2d0a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:40:38 -0600 Subject: [PATCH 0061/1178] Update key value for solution status --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2d5bde41329..ea49f7e26e4 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -331,7 +331,7 @@ def solve( legacy_results.solver.termination_condition = legacy_termination_condition_map[ results.termination_condition ] - legacy_soln.status = legacy_solution_status_map[results.termination_condition] + legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) obj = get_objective(model) From 51b97f5bc837689f5eafc8db2ca74bfeabcca556 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:45:54 -0600 Subject: [PATCH 0062/1178] Fix broken import statement --- pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 7e1d3e37af6..c1825879dbe 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,7 +1,7 @@ from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.solver.base import TerminationCondition +from pyomo.solver.results import TerminationCondition from pyomo.core.expr.taylor_series import taylor_series_expansion From 678df6fc48984ac81d399b8990320f8a00a40381 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:51:24 -0600 Subject: [PATCH 0063/1178] Correct one more broken import statement; apply black --- pyomo/contrib/appsi/examples/getting_started.py | 4 ++-- pyomo/solver/base.py | 10 ++++++---- pyomo/solver/results.py | 6 +----- pyomo/solver/tests/test_results.py | 4 +++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 79f1aa845b3..52f4992b37b 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,7 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer -from pyomo.solver import base as solver_base +from pyomo.solver import results def main(plot=True, n_points=200): @@ -34,7 +34,7 @@ def main(plot=True, n_points=200): res = opt.solve(m, timer=timer) assert ( res.termination_condition - == solver_base.TerminationCondition.convergenceCriteriaSatisfied + == results.TerminationCondition.convergenceCriteriaSatisfied ) obj_values.append(res.incumbent_objective) opt.load_vars([m.x]) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index ea49f7e26e4..9a7e19d7c85 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -29,7 +29,12 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.config import UpdateConfig from pyomo.solver.util import get_objective -from pyomo.solver.results import Results, legacy_solver_status_map, legacy_termination_condition_map, legacy_solution_status_map +from pyomo.solver.results import ( + Results, + legacy_solver_status_map, + legacy_termination_condition_map, + legacy_solution_status_map, +) class SolverBase(abc.ABC): @@ -279,9 +284,6 @@ def update_params(self): pass - - - class LegacySolverInterface: def solve( self, diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 0b6fdcafbc4..d51efa38168 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -19,9 +19,7 @@ NonNegativeFloat, ) from pyomo.solver.solution import SolutionLoader -from pyomo.opt.results.solution import ( - SolutionStatus as LegacySolutionStatus, -) +from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, @@ -221,5 +219,3 @@ def __str__(self): SolutionStatus.feasible: LegacySolutionStatus.feasible, SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, } - - diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 74c0f9f2256..7dea76c856f 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -90,7 +90,9 @@ def test_uninitialized(self): res = results.Results() self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) - self.assertEqual(res.termination_condition, results.TerminationCondition.unknown) + self.assertEqual( + res.termination_condition, results.TerminationCondition.unknown + ) self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) self.assertIsNone(res.solver_name) self.assertIsNone(res.solver_version) From f0d843cb62f9a5b1e5501c3b3db8a570505a4b8d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 10:10:50 -0600 Subject: [PATCH 0064/1178] Reorder imports for prettiness --- pyomo/solver/base.py | 3 ++- pyomo/solver/config.py | 1 + pyomo/solver/results.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 9a7e19d7c85..2bd6245feda 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -12,6 +12,8 @@ import abc import enum from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple +import os + from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -21,7 +23,6 @@ from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory -import os from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 32f6e1d5da0..d80e78eb2f2 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from typing import Optional + from pyomo.common.config import ( ConfigDict, ConfigValue, diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index d51efa38168..17e9416862e 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -11,6 +11,7 @@ import enum from typing import Optional + from pyomo.common.config import ( ConfigDict, ConfigValue, From 46d3c9095241f505540ec8e45069344021533e11 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 10:15:04 -0600 Subject: [PATCH 0065/1178] Copyright added; init updated --- pyomo/solver/__init__.py | 1 + pyomo/solver/config.py | 2 +- pyomo/solver/plugins.py | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index a3c2e0e95e8..e3eafa991cc 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -11,5 +11,6 @@ from . import base from . import config +from . import results from . import solution from . import util diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index d80e78eb2f2..22399caa35e 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -61,7 +61,7 @@ def __init__( self.declare('load_solution', ConfigValue(domain=bool, default=True)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool, default=False)) self.declare('report_timing', ConfigValue(domain=bool, default=False)) - self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) + self.declare('threads', ConfigValue(domain=NonNegativeInt)) self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 7e479474605..229488742cd 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -1,5 +1,16 @@ -from .base import SolverFactory +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from .base import SolverFactory def load(): pass From a619aca4ef54401d3e0658006fa569f35e6784e2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:07:12 -0600 Subject: [PATCH 0066/1178] Change several names; add type checking --- pyomo/contrib/appsi/solvers/cbc.py | 4 +-- pyomo/contrib/appsi/solvers/cplex.py | 8 +++--- pyomo/contrib/appsi/solvers/gurobi.py | 8 +++--- pyomo/contrib/appsi/solvers/highs.py | 8 +++--- pyomo/contrib/appsi/solvers/ipopt.py | 4 +-- pyomo/solver/base.py | 2 +- pyomo/solver/config.py | 40 ++++++++++++++++----------- pyomo/solver/plugins.py | 1 + pyomo/solver/results.py | 40 +++++++++++++++------------ pyomo/solver/solution.py | 2 ++ pyomo/solver/tests/test_config.py | 18 ++++++------ 11 files changed, 76 insertions(+), 59 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index c2686475b15..62404890d0b 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -23,7 +23,7 @@ from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import InterfaceConfig +from pyomo.solver.config import SolverConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) -class CbcConfig(InterfaceConfig): +class CbcConfig(SolverConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 86d50f1b82a..1837b5690a0 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -20,7 +20,7 @@ from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.config import BranchAndBoundConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) -class CplexConfig(MIPInterfaceConfig): +class CplexConfig(BranchAndBoundConfig): def __init__( self, description=None, @@ -263,8 +263,8 @@ def _process_stream(arg): if config.time_limit is not None: cplex_model.parameters.timelimit.set(config.time_limit) - if config.mip_gap is not None: - cplex_model.parameters.mip.tolerances.mipgap.set(config.mip_gap) + if config.rel_gap is not None: + cplex_model.parameters.mip.tolerances.mipgap.set(config.rel_gap) timer.start('cplex solve') t0 = time.time() diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 1f295dfcb49..99fa19820a5 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.config import BranchAndBoundConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils @@ -51,7 +51,7 @@ class DegreeError(PyomoException): pass -class GurobiConfig(MIPInterfaceConfig): +class GurobiConfig(BranchAndBoundConfig): def __init__( self, description=None, @@ -364,8 +364,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setParam('TimeLimit', config.time_limit) - if config.mip_gap is not None: - self._solver_model.setParam('MIPGap', config.mip_gap) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) for key, option in options.items(): self._solver_model.setParam(key, option) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index b5b2cc3b694..f62304563fc 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -21,7 +21,7 @@ from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.config import BranchAndBoundConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils @@ -35,7 +35,7 @@ class DegreeError(PyomoException): pass -class HighsConfig(MIPInterfaceConfig): +class HighsConfig(BranchAndBoundConfig): def __init__( self, description=None, @@ -219,8 +219,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setOptionValue('time_limit', config.time_limit) - if config.mip_gap is not None: - self._solver_model.setOptionValue('mip_rel_gap', config.mip_gap) + if config.rel_gap is not None: + self._solver_model.setOptionValue('mip_rel_gap', config.rel_gap) for key, option in options.items(): self._solver_model.setOptionValue(key, option) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index b16ca4dc792..569bb98457f 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -27,7 +27,7 @@ from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import InterfaceConfig +from pyomo.solver.config import SolverConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader @@ -35,7 +35,7 @@ logger = logging.getLogger(__name__) -class IpoptConfig(InterfaceConfig): +class IpoptConfig(SolverConfig): def __init__( self, description=None, diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2bd6245feda..ba25b23a354 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -130,7 +130,7 @@ def config(self): Returns ------- - InterfaceConfig + SolverConfig An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 22399caa35e..2f6f9b5bcc8 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from typing import Optional - from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -19,7 +17,7 @@ ) -class InterfaceConfig(ConfigDict): +class SolverConfig(ConfigDict): """ Attributes ---------- @@ -57,18 +55,24 @@ def __init__( visibility=visibility, ) - self.declare('tee', ConfigValue(domain=bool, default=False)) - self.declare('load_solution', ConfigValue(domain=bool, default=True)) - self.declare('symbolic_solver_labels', ConfigValue(domain=bool, default=False)) - self.declare('report_timing', ConfigValue(domain=bool, default=False)) - self.declare('threads', ConfigValue(domain=NonNegativeInt)) - - self.time_limit: Optional[float] = self.declare( + # TODO: Add in type-hinting everywhere + self.tee: bool = self.declare('tee', ConfigValue(domain=bool, default=False)) + self.load_solution: bool = self.declare( + 'load_solution', ConfigValue(domain=bool, default=True) + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) + ) + self.report_timing: bool = self.declare( + 'report_timing', ConfigValue(domain=bool, default=False) + ) + self.threads = self.declare('threads', ConfigValue(domain=NonNegativeInt)) + self.time_limit: NonNegativeFloat = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) -class MIPInterfaceConfig(InterfaceConfig): +class BranchAndBoundConfig(SolverConfig): """ Attributes ---------- @@ -95,11 +99,15 @@ def __init__( visibility=visibility, ) - self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) - self.declare('relax_integrality', ConfigValue(domain=bool)) - - self.mip_gap: Optional[float] = None - self.relax_integrality: bool = False + self.rel_gap: NonNegativeFloat = self.declare( + 'rel_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.abs_gap: NonNegativeFloat = self.declare( + 'abs_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.relax_integrality: bool = self.declare( + 'relax_integrality', ConfigValue(domain=bool, default=False) + ) class UpdateConfig(ConfigDict): diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 229488742cd..5120bc9dd36 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -12,5 +12,6 @@ from .base import SolverFactory + def load(): pass diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 17e9416862e..fa0080c5a7b 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -19,7 +19,6 @@ In, NonNegativeFloat, ) -from pyomo.solver.solution import SolutionLoader from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, @@ -128,17 +127,14 @@ def __init__( visibility=visibility, ) - self.declare( - 'solution_loader', - ConfigValue(default=SolutionLoader(None, None, None, None)), - ) - self.declare( + self.solution_loader = self.declare('solution_loader', ConfigValue()) + self.termination_condition: In(TerminationCondition) = self.declare( 'termination_condition', ConfigValue( domain=In(TerminationCondition), default=TerminationCondition.unknown ), ) - self.declare( + self.solution_status: In(SolutionStatus) = self.declare( 'solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), ) @@ -148,18 +144,28 @@ def __init__( self.objective_bound: Optional[float] = self.declare( 'objective_bound', ConfigValue(domain=float) ) - self.declare('solver_name', ConfigValue(domain=str)) - self.declare('solver_version', ConfigValue(domain=tuple)) - self.declare('termination_message', ConfigValue(domain=str)) - self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) - self.declare('timing_info', ConfigDict()) - # TODO: Set up type checking for start_time - self.timing_info.declare('start_time', ConfigValue()) - self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) - self.timing_info.declare( + self.solver_name: Optional[str] = self.declare( + 'solver_name', ConfigValue(domain=str) + ) + self.solver_version: Optional[tuple] = self.declare( + 'solver_version', ConfigValue(domain=tuple) + ) + self.iteration_count: NonNegativeInt = self.declare( + 'iteration_count', ConfigValue(domain=NonNegativeInt) + ) + self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) + self.timing_info.start_time = self.timing_info.declare( + 'start_time', ConfigValue() + ) + self.timing_info.wall_time: NonNegativeFloat = self.timing_info.declare( + 'wall_time', ConfigValue(domain=NonNegativeFloat) + ) + self.timing_info.solver_wall_time: NonNegativeFloat = self.timing_info.declare( 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) ) - self.declare('extra_info', ConfigDict(implicit=True)) + self.extra_info: ConfigDict = self.declare( + 'extra_info', ConfigDict(implicit=True) + ) def __str__(self): s = '' diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py index 1ef79050701..6c4b7431746 100644 --- a/pyomo/solver/solution.py +++ b/pyomo/solver/solution.py @@ -117,6 +117,8 @@ def get_reduced_costs( ) +# TODO: This is for development uses only; not to be released to the wild +# May turn into documentation someday class SolutionLoader(SolutionLoaderBase): def __init__( self, diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index 49d26513e2e..0686a3249d1 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -10,12 +10,12 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver.config import InterfaceConfig, MIPInterfaceConfig +from pyomo.solver.config import SolverConfig, BranchAndBoundConfig -class TestInterfaceConfig(unittest.TestCase): +class TestSolverConfig(unittest.TestCase): def test_interface_default_instantiation(self): - config = InterfaceConfig() + config = SolverConfig() self.assertEqual(config._description, None) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) @@ -24,7 +24,7 @@ def test_interface_default_instantiation(self): self.assertFalse(config.report_timing) def test_interface_custom_instantiation(self): - config = InterfaceConfig(description="A description") + config = SolverConfig(description="A description") config.tee = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") @@ -33,9 +33,9 @@ def test_interface_custom_instantiation(self): self.assertEqual(config.time_limit, 1.0) -class TestMIPInterfaceConfig(unittest.TestCase): +class TestBranchAndBoundConfig(unittest.TestCase): def test_interface_default_instantiation(self): - config = MIPInterfaceConfig() + config = BranchAndBoundConfig() self.assertEqual(config._description, None) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) @@ -46,12 +46,12 @@ def test_interface_default_instantiation(self): self.assertFalse(config.relax_integrality) def test_interface_custom_instantiation(self): - config = MIPInterfaceConfig(description="A description") + config = BranchAndBoundConfig(description="A description") config.tee = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") self.assertFalse(config.time_limit) config.time_limit = 1.0 self.assertEqual(config.time_limit, 1.0) - config.mip_gap = 2.5 - self.assertEqual(config.mip_gap, 2.5) + config.rel_gap = 2.5 + self.assertEqual(config.rel_gap, 2.5) From 5eb95a4f819751cce631a4bc114ee0e7bf90a29a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:15:23 -0600 Subject: [PATCH 0067/1178] Fix problematic attribute changes --- pyomo/solver/tests/test_config.py | 3 ++- pyomo/solver/tests/test_results.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index 0686a3249d1..c705c7cb8ac 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -42,7 +42,8 @@ def test_interface_default_instantiation(self): self.assertTrue(config.load_solution) self.assertFalse(config.symbolic_solver_labels) self.assertFalse(config.report_timing) - self.assertEqual(config.mip_gap, None) + self.assertEqual(config.rel_gap, None) + self.assertEqual(config.abs_gap, None) self.assertFalse(config.relax_integrality) def test_interface_custom_instantiation(self): diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 7dea76c856f..60b14d77521 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -12,6 +12,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.solver import results +from pyomo.solver import solution import pyomo.environ as pyo from pyomo.core.base.var import ScalarVar @@ -80,7 +81,6 @@ def test_declared_items(self): 'solver_name', 'solver_version', 'termination_condition', - 'termination_message', 'timing_info', } actual_declared = res._declared @@ -96,7 +96,6 @@ def test_uninitialized(self): self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) self.assertIsNone(res.solver_name) self.assertIsNone(res.solver_version) - self.assertIsNone(res.termination_message) self.assertIsNone(res.iteration_count) self.assertIsInstance(res.timing_info, ConfigDict) self.assertIsInstance(res.extra_info, ConfigDict) @@ -142,7 +141,7 @@ def test_results(self): slacks[m.c2] = 8 res = results.Results() - res.solution_loader = results.SolutionLoader( + res.solution_loader = solution.SolutionLoader( primals=primals, duals=duals, slacks=slacks, reduced_costs=rc ) From 198d0b1fb8d37d791f4fd89270772d4e56387b4c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:25:07 -0600 Subject: [PATCH 0068/1178] Change type hinting for several config/results objects --- pyomo/solver/config.py | 44 +++++++++++++++++------------------------ pyomo/solver/results.py | 23 ++++++++++++--------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 2f6f9b5bcc8..efd2a1bac16 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -9,6 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from typing import Optional + from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -55,7 +57,6 @@ def __init__( visibility=visibility, ) - # TODO: Add in type-hinting everywhere self.tee: bool = self.declare('tee', ConfigValue(domain=bool, default=False)) self.load_solution: bool = self.declare( 'load_solution', ConfigValue(domain=bool, default=True) @@ -66,8 +67,10 @@ def __init__( self.report_timing: bool = self.declare( 'report_timing', ConfigValue(domain=bool, default=False) ) - self.threads = self.declare('threads', ConfigValue(domain=NonNegativeInt)) - self.time_limit: NonNegativeFloat = self.declare( + self.threads: Optional[int] = self.declare( + 'threads', ConfigValue(domain=NonNegativeInt) + ) + self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) @@ -99,10 +102,10 @@ def __init__( visibility=visibility, ) - self.rel_gap: NonNegativeFloat = self.declare( + self.rel_gap: Optional[float] = self.declare( 'rel_gap', ConfigValue(domain=NonNegativeFloat) ) - self.abs_gap: NonNegativeFloat = self.declare( + self.abs_gap: Optional[float] = self.declare( 'abs_gap', ConfigValue(domain=NonNegativeFloat) ) self.relax_integrality: bool = self.declare( @@ -143,7 +146,7 @@ def __init__( visibility=visibility, ) - self.declare( + self.check_for_new_or_removed_constraints: bool = self.declare( 'check_for_new_or_removed_constraints', ConfigValue( domain=bool, @@ -155,7 +158,7 @@ def __init__( added to/removed from the model.""", ), ) - self.declare( + self.check_for_new_or_removed_vars: bool = self.declare( 'check_for_new_or_removed_vars', ConfigValue( domain=bool, @@ -167,7 +170,7 @@ def __init__( removed from the model.""", ), ) - self.declare( + self.check_for_new_or_removed_params: bool = self.declare( 'check_for_new_or_removed_params', ConfigValue( domain=bool, @@ -179,7 +182,7 @@ def __init__( removed from the model.""", ), ) - self.declare( + self.check_for_new_objective: bool = self.declare( 'check_for_new_objective', ConfigValue( domain=bool, @@ -190,7 +193,7 @@ def __init__( when you are certain objectives are not being added to / removed from the model.""", ), ) - self.declare( + self.update_constraints: bool = self.declare( 'update_constraints', ConfigValue( domain=bool, @@ -203,7 +206,7 @@ def __init__( are not being modified.""", ), ) - self.declare( + self.update_vars: bool = self.declare( 'update_vars', ConfigValue( domain=bool, @@ -215,7 +218,7 @@ def __init__( opt.update_variables() or when you are certain variables are not being modified.""", ), ) - self.declare( + self.update_params: bool = self.declare( 'update_params', ConfigValue( domain=bool, @@ -226,7 +229,7 @@ def __init__( opt.update_params() or when you are certain parameters are not being modified.""", ), ) - self.declare( + self.update_named_expressions: bool = self.declare( 'update_named_expressions', ConfigValue( domain=bool, @@ -238,7 +241,7 @@ def __init__( Expressions are not being modified.""", ), ) - self.declare( + self.update_objective: bool = self.declare( 'update_objective', ConfigValue( domain=bool, @@ -250,7 +253,7 @@ def __init__( certain objectives are not being modified.""", ), ) - self.declare( + self.treat_fixed_vars_as_params: bool = self.declare( 'treat_fixed_vars_as_params', ConfigValue( domain=bool, @@ -267,14 +270,3 @@ def __init__( updating the values of fixed variables is much faster this way.""", ), ) - - self.check_for_new_or_removed_constraints: bool = True - self.check_for_new_or_removed_vars: bool = True - self.check_for_new_or_removed_params: bool = True - self.check_for_new_objective: bool = True - self.update_constraints: bool = True - self.update_vars: bool = True - self.update_params: bool = True - self.update_named_expressions: bool = True - self.update_objective: bool = True - self.treat_fixed_vars_as_params: bool = True diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index fa0080c5a7b..02a898f2df5 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -10,7 +10,8 @@ # ___________________________________________________________________________ import enum -from typing import Optional +from typing import Optional, Tuple +from datetime import datetime from pyomo.common.config import ( ConfigDict, @@ -24,6 +25,7 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) +from pyomo.solver.solution import SolutionLoaderBase class TerminationCondition(enum.Enum): @@ -127,14 +129,16 @@ def __init__( visibility=visibility, ) - self.solution_loader = self.declare('solution_loader', ConfigValue()) - self.termination_condition: In(TerminationCondition) = self.declare( + self.solution_loader: SolutionLoaderBase = self.declare( + 'solution_loader', ConfigValue() + ) + self.termination_condition: TerminationCondition = self.declare( 'termination_condition', ConfigValue( domain=In(TerminationCondition), default=TerminationCondition.unknown ), ) - self.solution_status: In(SolutionStatus) = self.declare( + self.solution_status: SolutionStatus = self.declare( 'solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), ) @@ -147,20 +151,21 @@ def __init__( self.solver_name: Optional[str] = self.declare( 'solver_name', ConfigValue(domain=str) ) - self.solver_version: Optional[tuple] = self.declare( + self.solver_version: Optional[Tuple[int, ...]] = self.declare( 'solver_version', ConfigValue(domain=tuple) ) - self.iteration_count: NonNegativeInt = self.declare( + self.iteration_count: Optional[int] = self.declare( 'iteration_count', ConfigValue(domain=NonNegativeInt) ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) - self.timing_info.start_time = self.timing_info.declare( + # TODO: Implement type checking for datetime + self.timing_info.start_time: datetime = self.timing_info.declare( 'start_time', ConfigValue() ) - self.timing_info.wall_time: NonNegativeFloat = self.timing_info.declare( + self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) ) - self.timing_info.solver_wall_time: NonNegativeFloat = self.timing_info.declare( + self.timing_info.solver_wall_time: Optional[float] = self.timing_info.declare( 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) ) self.extra_info: ConfigDict = self.declare( From 6dbc84d3f0064cb133fff46a62b0c14a0495dfd2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:35:34 -0600 Subject: [PATCH 0069/1178] Change un-init test to assign an empty SolutionLoader --- pyomo/solver/tests/test_results.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 60b14d77521..f43b2b50ef4 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -102,6 +102,7 @@ def test_uninitialized(self): self.assertIsNone(res.timing_info.start_time) self.assertIsNone(res.timing_info.wall_time) self.assertIsNone(res.timing_info.solver_wall_time) + res.solution_loader = solution.SolutionLoader(None, None, None, None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' From d09c88040db806eeb91eac478b3267e07474ad01 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 14:51:58 -0600 Subject: [PATCH 0070/1178] SAVE POINT: Starting work on IPOPT solver re-write --- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/solver/IPOPT.py | 123 +++++++++++++++++++++++ pyomo/solver/__init__.py | 1 + pyomo/solver/base.py | 22 +--- pyomo/solver/config.py | 3 + pyomo/solver/factory.py | 33 ++++++ pyomo/solver/plugins.py | 2 +- pyomo/solver/tests/solvers/test_ipopt.py | 48 +++++++++ pyomo/solver/util.py | 9 ++ 9 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 pyomo/solver/IPOPT.py create mode 100644 pyomo/solver/factory.py create mode 100644 pyomo/solver/tests/solvers/test_ipopt.py diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 86dcd298a93..3a132b74395 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.solver.base import SolverFactory +from pyomo.solver.factory import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py new file mode 100644 index 00000000000..3f5fa0e1df6 --- /dev/null +++ b/pyomo/solver/IPOPT.py @@ -0,0 +1,123 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import os +import subprocess + +from pyomo.common import Executable +from pyomo.common.config import ConfigValue +from pyomo.common.tempfiles import TempfileManager +from pyomo.opt import WriterFactory +from pyomo.solver.base import SolverBase +from pyomo.solver.config import SolverConfig +from pyomo.solver.factory import SolverFactory +from pyomo.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.solver.solution import SolutionLoaderBase +from pyomo.solver.util import SolverSystemError + +import logging + +logger = logging.getLogger(__name__) + + +class IPOPTConfig(SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.executable = self.declare( + 'executable', ConfigValue(default=Executable('ipopt')) + ) + self.save_solver_io: bool = self.declare( + 'save_solver_io', ConfigValue(domain=bool, default=False) + ) + + +class IPOPTSolutionLoader(SolutionLoaderBase): + pass + + +@SolverFactory.register('ipopt', doc='The IPOPT NLP solver (new interface)') +class IPOPT(SolverBase): + CONFIG = IPOPTConfig() + + def __init__(self, **kwds): + self.config = self.CONFIG(kwds) + + def available(self): + if self.config.executable.path() is None: + return self.Availability.NotFound + return self.Availability.FullLicense + + def version(self): + results = subprocess.run( + [str(self.config.executable), '--version'], + timeout=1, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1] + version = version.strip() + version = tuple(int(i) for i in version.split('.')) + return version + + @property + def config(self): + return self._config + + @config.setter + def config(self, val): + self._config = val + + def solve(self, model, **kwds): + # Check if solver is available + avail = self.available() + if not avail: + raise SolverSystemError( + f'Solver {self.__class__} is not available ({avail}).' + ) + # Update configuration options, based on keywords passed to solve + config = self.config(kwds.pop('options', {})) + config.set_value(kwds) + # Write the model to an nl file + nl_writer = WriterFactory('nl') + # Need to add check for symbolic_solver_labels; may need to generate up + # to three files for nl, row, col, if ssl == True + # What we have here may or may not work with IPOPT; will find out when + # we try to run it. + with TempfileManager.new_context() as tempfile: + dname = tempfile.mkdtemp() + with open(os.path.join(dname, model.name + '.nl')) as nl_file, open( + os.path.join(dname, model.name + '.row') + ) as row_file, open(os.path.join(dname, model.name + '.col')) as col_file: + info = nl_writer.write( + model, + nl_file, + row_file, + col_file, + symbolic_solver_labels=config.symbolic_solver_labels, + ) + # Call IPOPT - passing the files via the subprocess + subprocess.run() diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index e3eafa991cc..1ab9f975f0b 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -11,6 +11,7 @@ from . import base from . import config +from . import factory from . import results from . import solution from . import util diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index ba25b23a354..f7e5c4c58c5 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -21,8 +21,7 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError -from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory + from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -442,22 +441,3 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass - - -class SolverFactoryClass(Factory): - def register(self, name, doc=None): - def decorator(cls): - self._cls[name] = cls - self._doc[name] = doc - - class LegacySolver(LegacySolverInterface, cls): - pass - - LegacySolverFactory.register(name, doc)(LegacySolver) - - return cls - - return decorator - - -SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index efd2a1bac16..ed9008b7e1f 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -73,6 +73,9 @@ def __init__( self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', ConfigDict(implicit=True) + ) class BranchAndBoundConfig(SolverConfig): diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py new file mode 100644 index 00000000000..1a49ea92e40 --- /dev/null +++ b/pyomo/solver/factory.py @@ -0,0 +1,33 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +from pyomo.solver.base import LegacySolverInterface + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverInterface, cls): + pass + + LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 5120bc9dd36..5dfd4bce1eb 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from .base import SolverFactory +from .factory import SolverFactory def load(): diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py new file mode 100644 index 00000000000..afe2dbbe531 --- /dev/null +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -0,0 +1,48 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +import pyomo.environ as pyo +from pyomo.common.fileutils import ExecutableData +from pyomo.common.config import ConfigDict +from pyomo.solver.IPOPT import IPOPTConfig +from pyomo.solver.factory import SolverFactory +from pyomo.common import unittest + + +class TestIPOPT(unittest.TestCase): + def create_model(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(m): + return (1.0 - m.x) ** 2 + 100.0 * (m.y - m.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + return model + + def test_IPOPT_config(self): + # Test default initialization + config = IPOPTConfig() + self.assertTrue(config.load_solution) + self.assertIsInstance(config.solver_options, ConfigDict) + print(type(config.executable)) + self.assertIsInstance(config.executable, ExecutableData) + + # Test custom initialization + solver = SolverFactory('ipopt', save_solver_io=True) + self.assertTrue(solver.config.save_solver_io) + self.assertFalse(solver.config.tee) + + # Change value on a solve call + # model = self.create_model() + # result = solver.solve(model, tee=True) diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 1fb1738470b..79abee1b689 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -20,11 +20,20 @@ from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.collections import ComponentMap +from pyomo.common.errors import PyomoException from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant from pyomo.solver.config import UpdateConfig +class SolverSystemError(PyomoException): + """ + General exception to catch solver system errors + """ + + pass + + def get_objective(block): obj = None for o in block.component_data_objects( From f474d49008342b108e91bfde111beaf65434592f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 15:29:21 -0600 Subject: [PATCH 0071/1178] Change SolverFactory to remove legacy solver references --- pyomo/solver/factory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py index 1a49ea92e40..84b6cf02eac 100644 --- a/pyomo/solver/factory.py +++ b/pyomo/solver/factory.py @@ -20,10 +20,10 @@ def decorator(cls): self._cls[name] = cls self._doc[name] = doc - class LegacySolver(LegacySolverInterface, cls): - pass + # class LegacySolver(LegacySolverInterface, cls): + # pass - LegacySolverFactory.register(name, doc)(LegacySolver) + # LegacySolverFactory.register(name, doc)(LegacySolver) return cls From 88aeba6c00154a397570956524e9c0479c99024b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 15:54:36 -0600 Subject: [PATCH 0072/1178] SAVE POINT: Stopping for end of sprin --- pyomo/solver/IPOPT.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 3f5fa0e1df6..384b2173840 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -101,6 +101,14 @@ def solve(self, model, **kwds): # Update configuration options, based on keywords passed to solve config = self.config(kwds.pop('options', {})) config.set_value(kwds) + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if 'PYOMO_AMPLFUNC' in env: + env['AMPLFUNC'] = "\n".join( + filter( + None, (env.get('AMPLFUNC', None), env.get('PYOMO_AMPLFUNC', None)) + ) + ) # Write the model to an nl file nl_writer = WriterFactory('nl') # Need to add check for symbolic_solver_labels; may need to generate up @@ -112,7 +120,7 @@ def solve(self, model, **kwds): with open(os.path.join(dname, model.name + '.nl')) as nl_file, open( os.path.join(dname, model.name + '.row') ) as row_file, open(os.path.join(dname, model.name + '.col')) as col_file: - info = nl_writer.write( + self.info = nl_writer.write( model, nl_file, row_file, @@ -120,4 +128,29 @@ def solve(self, model, **kwds): symbolic_solver_labels=config.symbolic_solver_labels, ) # Call IPOPT - passing the files via the subprocess - subprocess.run() + cmd = [str(config.executable), nl_file, '-AMPL'] + if config.time_limit is not None: + config.solver_options['max_cpu_time'] = config.time_limit + for key, val in config.solver_options.items(): + cmd.append(key + '=' + val) + process = subprocess.run(cmd, timeout=config.time_limit, + env=env, + universal_newlines=True) + + if process.returncode != 0: + if self.config.load_solution: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set config.load_solution=False and check ' + 'results.termination_condition and ' + 'results.incumbent_objective before loading a solution.' + ) + results = Results() + results.termination_condition = TerminationCondition.error + else: + results = self._parse_solution() + + def _parse_solution(self): + # STOPPING POINT: The suggestion here is to look at the original + # parser, which hasn't failed yet, and rework it to be ... better? + pass From 553c8bb45741c9a8c55a9ee68f4aa349aaa144aa Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 1 Sep 2023 15:34:14 -0400 Subject: [PATCH 0073/1178] add deactivate_trivial_constraints for feasibility subproblem --- pyomo/contrib/mindtpy/algorithm_base_class.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 7def1dcaab3..584d796d069 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1311,6 +1311,20 @@ def solve_feasibility_subproblem(self): update_solver_timelimit( self.feasibility_nlp_opt, config.nlp_solver, self.timing, config ) + try: + TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( + self.fixed_nlp, + tmp=True, + ignore_infeasible=False, + tolerance=config.constraint_tolerance, + ) + except InfeasibleConstraintException as e: + config.logger.error( + str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + ) + results = SolverResults() + results.solver.termination_condition = tc.infeasible + return self.fixed_nlp, results with SuppressInfeasibleWarning(): try: with time_code(self.timing, 'feasibility subproblem'): @@ -1341,6 +1355,9 @@ def solve_feasibility_subproblem(self): self.handle_feasibility_subproblem_tc( feas_soln.solver.termination_condition, MindtPy ) + TransformationFactory('contrib.deactivate_trivial_constraints').revert( + self.fixed_nlp + ) MindtPy.feas_opt.deactivate() for constr in MindtPy.nonlinear_constraint_list: constr.activate() From b7a446c70dcbadd43a4da5e330a8e39e785f0d71 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 8 Sep 2023 11:08:59 -0600 Subject: [PATCH 0074/1178] Update documentation on solver interfaces results --- .../developer_reference/solvers.rst | 36 +++++- pyomo/solver/IPOPT.py | 6 +- pyomo/solver/results.py | 116 ++++++++++++++---- 3 files changed, 130 insertions(+), 28 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index d48e270cc7c..1dcd2f66da7 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -3,17 +3,47 @@ Solver Interfaces Pyomo offers interfaces into multiple solvers, both commercial and open source. +.. currentmodule:: pyomo.solver + + +Results +------- + +Every solver, at the end of a ``solve`` call, will return a ``Results`` object. +This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar +to a standard ``dict`` in Python. + +.. autoclass:: pyomo.solver.results.Results + :show-inheritance: + :members: + :undoc-members: + Termination Conditions ---------------------- Pyomo offers a standard set of termination conditions to map to solver -returns. +returns. The intent of ``TerminationCondition`` is to notify the user of why +the solver exited. The user is expected to inspect the ``Results`` object or any +returned solver messages or logs for more information. -.. currentmodule:: pyomo.contrib.appsi -.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition + +.. autoclass:: pyomo.solver.results.TerminationCondition + :show-inheritance: :noindex: +Solution Status +--------------- + +Pyomo offers a standard set of solution statuses to map to solver output. The +intent of ``SolutionStatus`` is to notify the user of what the solver returned +at a high level. The user is expected to inspect the ``Results`` object or any +returned solver messages or logs for more information. + +.. autoclass:: pyomo.solver.results.SolutionStatus + :show-inheritance: + :noindex: + diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 384b2173840..cce0017c5be 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -133,9 +133,9 @@ def solve(self, model, **kwds): config.solver_options['max_cpu_time'] = config.time_limit for key, val in config.solver_options.items(): cmd.append(key + '=' + val) - process = subprocess.run(cmd, timeout=config.time_limit, - env=env, - universal_newlines=True) + process = subprocess.run( + cmd, timeout=config.time_limit, env=env, universal_newlines=True + ) if process.returncode != 0: if self.config.load_solution: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 02a898f2df5..6a940860661 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -30,68 +30,104 @@ class TerminationCondition(enum.Enum): """ - An enumeration for checking the termination condition of solvers - """ + An Enum that enumerates all possible exit statuses for a solver call. - """unknown serves as both a default value, and it is used when no other enum member makes sense""" - unknown = 42 + Attributes + ---------- + convergenceCriteriaSatisfied: 0 + The solver exited because convergence criteria of the problem were + satisfied. + maxTimeLimit: 1 + The solver exited due to reaching a specified time limit. + iterationLimit: 2 + The solver exited due to reaching a specified iteration limit. + objectiveLimit: 3 + The solver exited due to reaching an objective limit. For example, + in Gurobi, the exit message "Optimal objective for model was proven to + be worse than the value specified in the Cutoff parameter" would map + to objectiveLimit. + minStepLength: 4 + The solver exited due to a minimum step length. + Minimum step length reached may mean that the problem is infeasible or + that the problem is feasible but the solver could not converge. + unbounded: 5 + The solver exited because the problem has been found to be unbounded. + provenInfeasible: 6 + The solver exited because the problem has been proven infeasible. + locallyInfeasible: 7 + The solver exited because no feasible solution was found to the + submitted problem, but it could not be proven that no such solution exists. + infeasibleOrUnbounded: 8 + Some solvers do not specify between infeasibility or unboundedness and + instead return that one or the other has occurred. For example, in + Gurobi, this may occur because there are some steps in presolve that + prevent Gurobi from distinguishing between infeasibility and unboundedness. + error: 9 + The solver exited with some error. The error message will also be + captured and returned. + interrupted: 10 + The solver was interrupted while running. + licensingProblems: 11 + The solver experienced issues with licensing. This could be that no + license was found, the license is of the wrong type for the problem (e.g., + problem is too big for type of license), or there was an issue contacting + a licensing server. + unknown: 42 + All other unrecognized exit statuses fall in this category. + """ - """The solver exited because the convergence criteria were satisfied""" convergenceCriteriaSatisfied = 0 - """The solver exited due to a time limit""" maxTimeLimit = 1 - """The solver exited due to an iteration limit""" iterationLimit = 2 - """The solver exited due to an objective limit""" objectiveLimit = 3 - """The solver exited due to a minimum step length""" minStepLength = 4 - """The solver exited because the problem is unbounded""" unbounded = 5 - """The solver exited because the problem is proven infeasible""" provenInfeasible = 6 - """The solver exited because the problem was found to be locally infeasible""" locallyInfeasible = 7 - """The solver exited because the problem is either infeasible or unbounded""" infeasibleOrUnbounded = 8 - """The solver exited due to an error""" error = 9 - """The solver exited because it was interrupted""" interrupted = 10 - """The solver exited due to licensing problems""" licensingProblems = 11 + unknown = 42 + class SolutionStatus(enum.IntEnum): """ An enumeration for interpreting the result of a termination. This describes the designated status by the solver to be loaded back into the model. - For now, we are choosing to use IntEnum such that return values are numerically - assigned in increasing order. + Attributes + ---------- + noSolution: 0 + No (single) solution was found; possible that a population of solutions + was returned. + infeasible: 10 + Solution point does not satisfy some domains and/or constraints. + feasible: 20 + A solution for which all of the constraints in the model are satisfied. + optimal: 30 + A feasible solution where the objective function reaches its specified + sense (e.g., maximum, minimum) """ - """No (single) solution found; possible that a population of solutions was returned""" noSolution = 0 - """Solution point does not satisfy some domains and/or constraints""" infeasible = 10 - """Feasible solution identified""" feasible = 20 - """Optimal solution identified""" optimal = 30 @@ -99,9 +135,14 @@ class Results(ConfigDict): """ Attributes ---------- + solution_loader: SolutionLoaderBase + Object for loading the solution back into the model. termination_condition: TerminationCondition The reason the solver exited. This is a member of the TerminationCondition enum. + solution_status: SolutionStatus + The result of the solve call. This is a member of the SolutionStatus + enum. incumbent_objective: float If a feasible solution was found, this is the objective value of the best solution found. If no feasible solution was found, this is @@ -111,6 +152,19 @@ class Results(ConfigDict): the lower bound. For maximization problems, this is the upper bound. For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) + solver_name: str + The name of the solver in use. + solver_version: tuple + A tuple representing the version of the solver in use. + iteration_count: int + The total number of iterations. + timing_info: ConfigDict + A ConfigDict containing three pieces of information: + start_time: UTC timestamp of when run was initiated + wall_time: elapsed wall clock time for entire process + solver_wall_time: elapsed wall clock time for solve call + extra_info: ConfigDict + A ConfigDict to store extra information such as solver messages. """ def __init__( @@ -181,6 +235,24 @@ def __str__(self): return s +class ResultsReader: + pass + + +def parse_sol_file(filename, results): + if results is None: + results = Results() + pass + + +def parse_yaml(): + pass + + +def parse_json(): + pass + + # Everything below here preserves backwards compatibility legacy_termination_condition_map = { From b3af7ac380aa58d9d40393890b0436438e7305ff Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 8 Sep 2023 11:14:10 -0600 Subject: [PATCH 0075/1178] Add in TODOs for documentation --- doc/OnlineDocs/developer_reference/solvers.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 1dcd2f66da7..75d95fc36db 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -6,6 +6,12 @@ Pyomo offers interfaces into multiple solvers, both commercial and open source. .. currentmodule:: pyomo.solver +Interface Implementation +------------------------ + +TBD: How to add a new interface; the pieces. + + Results ------- @@ -20,7 +26,7 @@ to a standard ``dict`` in Python. Termination Conditions ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ Pyomo offers a standard set of termination conditions to map to solver returns. The intent of ``TerminationCondition`` is to notify the user of why @@ -35,7 +41,7 @@ returned solver messages or logs for more information. Solution Status ---------------- +^^^^^^^^^^^^^^^ Pyomo offers a standard set of solution statuses to map to solver output. The intent of ``SolutionStatus`` is to notify the user of what the solver returned @@ -47,3 +53,7 @@ returned solver messages or logs for more information. :noindex: +Solution +-------- + +TBD: How to load/parse a solution. From 5c05c7880f8281e52cf8cc8cd8407374e5313998 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 12 Sep 2023 12:44:11 -0600 Subject: [PATCH 0076/1178] SAVE POINT: Start the termination conditions/etc. --- pyomo/solver/results.py | 68 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 6a940860661..c8c92109040 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -26,6 +26,7 @@ SolverStatus as LegacySolverStatus, ) from pyomo.solver.solution import SolutionLoaderBase +from pyomo.solver.util import SolverSystemError class TerminationCondition(enum.Enum): @@ -239,10 +240,73 @@ class ResultsReader: pass -def parse_sol_file(filename, results): +def parse_sol_file(file, results): + # The original reader for sol files is in pyomo.opt.plugins.sol. + # Per my original complaint, it has "magic numbers" that I just don't + # know how to test. It's apparently less fragile than that in APPSI. + # NOTE: The Results object now also holds the solution loader, so we do + # not need pass in a solution like we did previously. if results is None: results = Results() - pass + + # For backwards compatibility and general safety, we will parse all + # lines until "Options" appears. Anything before "Options" we will + # consider to be the solver message. + message = [] + for line in file: + if not line: + break + line = line.strip() + if "Options" in line: + break + message.append(line) + message = '\n'.join(message) + # Once "Options" appears, we must now read the content under it. + model_objects = [] + if "Options" in line: + line = file.readline() + number_of_options = int(line) + need_tolerance = False + if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is + number_of_options -= 2 + need_tolerance = True + for i in range(number_of_options + 4): + line = file.readline() + model_objects.append(int(line)) + if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is + line = file.readline() + model_objects.append(float(line)) + else: + raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") + # Identify the total number of variables and constraints + number_of_cons = model_objects[number_of_options + 1] + number_of_vars = model_objects[number_of_options + 3] + constraints = [] + variables = [] + # Parse through the constraint lines and capture the constraints + i = 0 + while i < number_of_cons: + line = file.readline() + constraints.append(float(line)) + # Parse through the variable lines and capture the variables + i = 0 + while i < number_of_vars: + line = file.readline() + variables.append(float(line)) + exit_code = [0, 0] + line = file.readline() + if line and ('objno' in line): + exit_code_line = line.split() + if (len(exit_code_line) != 3): + raise SolverSystemError(f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}.") + exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + else: + raise SolverSystemError(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") + results.extra_info.solver_message = message.strip().replace('\n', '; ') + # Not sure if next two lines are needed + # if isinstance(res.solver.message, str): + # res.solver.message = res.solver.message.replace(':', '\\x3a') + def parse_yaml(): From 472b5dfee6a58497b566d3d692d1fac209c6b4b3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 26 Sep 2023 08:27:44 -0600 Subject: [PATCH 0077/1178] Save point: working on writer --- pyomo/solver/IPOPT.py | 3 +-- pyomo/solver/results.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index cce0017c5be..6501154d7ac 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -78,8 +78,7 @@ def version(self): universal_newlines=True, ) version = results.stdout.splitlines()[0] - version = version.split(' ')[1] - version = version.strip() + version = version.split(' ')[1].strip() version = tuple(int(i) for i in version.split('.')) return version diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index c8c92109040..2fa62027e6c 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -293,6 +293,7 @@ def parse_sol_file(file, results): while i < number_of_vars: line = file.readline() variables.append(float(line)) + # Parse the exit code line and capture it exit_code = [0, 0] line = file.readline() if line and ('objno' in line): @@ -306,8 +307,34 @@ def parse_sol_file(file, results): # Not sure if next two lines are needed # if isinstance(res.solver.message, str): # res.solver.message = res.solver.message.replace(':', '\\x3a') + if (exit_code[1] >= 0) and (exit_code[1] <= 99): + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal + elif (exit_code[1] >= 100) and (exit_code[1] <= 199): + exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal + if results.extra_info.solver_message: + results.extra_info.solver_message += '; ' + exit_code_message + else: + results.extra_info.solver_message = exit_code_message + elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + results.termination_condition = TerminationCondition.locallyInfeasible + results.solution_status = SolutionStatus.infeasible + elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + results.termination_condition = TerminationCondition.unbounded + results.solution_status = SolutionStatus.infeasible + elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + results.solver.termination_condition = TerminationCondition.iterationLimit + elif (exit_code[1] >= 500) and (exit_code[1] <= 599): + exit_code_message = ( + "FAILURE: the solver stopped by an error condition " + "in the solver routines!" + ) + results.solver.termination_condition = TerminationCondition.error + return results - + return results def parse_yaml(): pass From 4061af9df05e2565931f93f3366adf08e9e6f998 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 2 Oct 2023 15:14:41 -0600 Subject: [PATCH 0078/1178] SAVE POINT: Adding Datetime checker --- pyomo/common/config.py | 11 +++++++++++ pyomo/solver/results.py | 9 +++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 61e4f682a2a..1e11fbdc431 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -18,6 +18,7 @@ import argparse import builtins +import datetime import enum import importlib import inspect @@ -203,6 +204,16 @@ def NonNegativeFloat(val): return ans +def Datetime(val): + """Domain validation function to check for datetime.datetime type. + + This domain will return the original object, assuming it is of the right type. + """ + if not isinstance(val, datetime.datetime): + raise ValueError(f"Expected datetime object, but received {type(val)}.") + return val + + class In(object): """In(domain, cast=None) Domain validation class admitting a Container of possible values diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 2fa62027e6c..8e4b6cf21a7 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -16,6 +16,7 @@ from pyomo.common.config import ( ConfigDict, ConfigValue, + Datetime, NonNegativeInt, In, NonNegativeFloat, @@ -213,9 +214,9 @@ def __init__( 'iteration_count', ConfigValue(domain=NonNegativeInt) ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) - # TODO: Implement type checking for datetime + self.timing_info.start_time: datetime = self.timing_info.declare( - 'start_time', ConfigValue() + 'start_time', ConfigValue(domain=Datetime) ) self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) @@ -331,6 +332,10 @@ def parse_sol_file(file, results): "FAILURE: the solver stopped by an error condition " "in the solver routines!" ) + if results.extra_info.solver_message: + results.extra_info.solver_message += '; ' + exit_code_message + else: + results.extra_info.solver_message = exit_code_message results.solver.termination_condition = TerminationCondition.error return results From b5af408e17d0b65f4b270397817be5343f6edd3e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 2 Oct 2023 15:16:39 -0600 Subject: [PATCH 0079/1178] Swap FullLicense and LimitedLicense --- pyomo/solver/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index f7e5c4c58c5..07f19fbb58c 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -39,11 +39,11 @@ class SolverBase(abc.ABC): class Availability(enum.IntEnum): + FullLicense = 2 + LimitedLicense = 1 NotFound = 0 BadVersion = -1 BadLicense = -2 - FullLicense = 1 - LimitedLicense = 2 NeedsCompiledExtension = -3 def __bool__(self): From c100bf917e9a31b90611041c707e5902d0cc04da Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 14:34:46 -0600 Subject: [PATCH 0080/1178] Resolve broken import --- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 6451db18087..1fb9b87de2e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -7,7 +7,6 @@ from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output from pyomo.contrib.appsi.solvers.highs import Highs -from pyomo.contrib.appsi.base import TerminationCondition opt = Highs() From 332d28694e1acc8247262539e5a03b87f52596e1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 14:51:57 -0600 Subject: [PATCH 0081/1178] Resolving conflicts again: stream_solver -> tee --- pyomo/contrib/appsi/solvers/highs.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index da4ea8c130a..3d2104cdbfa 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -349,7 +349,7 @@ def set_instance(self, model): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: with capture_output(output=t.STDOUT, capture_fd=True): diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 1fb9b87de2e..da39a5c3d55 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -94,7 +94,7 @@ def test_capture_highs_output(self): model[-2:-1] = [ 'opt = Highs()', - 'opt.config.stream_solver = True', + 'opt.config.tee = True', 'result = opt.solve(m)', ] with LoggingIntercept() as LOG, capture_output(capture_fd=True) as OUT: From 0b348a0ac8a73fdaae9e628aefa17446c23b9584 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 15:09:40 -0600 Subject: [PATCH 0082/1178] Resolve convergence of APPSI and new Results object --- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 6 +++--- .../contrib/appsi/solvers/tests/test_persistent_solvers.py | 4 ++-- pyomo/solver/tests/test_results.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index da39a5c3d55..25b7ae91b86 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -37,7 +37,7 @@ def test_mutable_params_with_remove_cons(self): del m.c1 m.p2.value = 2 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -8) + self.assertAlmostEqual(res.incumbent_objective, -8) def test_mutable_params_with_remove_vars(self): m = pe.ConcreteModel() @@ -59,14 +59,14 @@ def test_mutable_params_with_remove_vars(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) del m.c1 del m.c2 m.p1.value = -9 m.p2.value = 9 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -9) + self.assertAlmostEqual(res.incumbent_objective, -9) def test_capture_highs_output(self): # tests issue #3003 diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 1b9f5c3b0a2..299a5bd5b7e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1354,13 +1354,13 @@ def test_bug_2(self, name: str, opt_class: Type[PersistentSolverBase], only_chil m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 2, 5) + self.assertAlmostEqual(res.incumbent_objective, 2, 5) m.x.unfix() m.x.setlb(-9) m.x.setub(9) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -18, 5) + self.assertAlmostEqual(res.incumbent_objective, -18, 5) @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index f43b2b50ef4..5392c1135f8 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -35,7 +35,7 @@ def test_member_list(self): 'interrupted', 'licensingProblems', ] - self.assertEqual(member_list, expected_list) + self.assertEqual(member_list.sort(), expected_list.sort()) def test_codes(self): self.assertEqual(results.TerminationCondition.unknown.value, 42) From 457c5993dcfe73ee95064a54ac9e41a33fe9e0e8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 15:19:55 -0600 Subject: [PATCH 0083/1178] Resolve one more convergence error --- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 25b7ae91b86..cd65783c566 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -32,7 +32,7 @@ def test_mutable_params_with_remove_cons(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) del m.c1 m.p2.value = 2 From a58dc41a3d15eec8cda39cc5642b34ad39fd45f0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 26 Oct 2023 08:39:42 -0600 Subject: [PATCH 0084/1178] Obvious bug fix --- pyomo/solver/IPOPT.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 6501154d7ac..875f8710b10 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -84,11 +84,11 @@ def version(self): @property def config(self): - return self._config + return self.config @config.setter def config(self, val): - self._config = val + self.config = val def solve(self, model, **kwds): # Check if solver is available From 52cb54f9418b081f44f1a3887e6dae03ebf9710f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 11:45:20 -0600 Subject: [PATCH 0085/1178] Fixing some places where we mix up binaries and Booleans in the hull tests --- pyomo/gdp/tests/test_hull.py | 72 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 09f65765fe6..b7b5a11e28c 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -510,10 +510,10 @@ def test_disaggregatedVar_mappings(self): for i in [0, 1]: mappings = ComponentMap() mappings[m.x] = disjBlock[i].disaggregatedVars.x - if i == 1: # this disjunct as x, w, and no y + if i == 1: # this disjunct has x, w, and no y mappings[m.w] = disjBlock[i].disaggregatedVars.w mappings[m.y] = transBlock._disaggregatedVars[0] - elif i == 0: # this disjunct as x, y, and no w + elif i == 0: # this disjunct has x, y, and no w mappings[m.y] = disjBlock[i].disaggregatedVars.y mappings[m.w] = transBlock._disaggregatedVars[1] @@ -1427,16 +1427,16 @@ def test_relaxation_feasibility(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1468,16 +1468,16 @@ def test_relaxation_feasibility_transform_inner_first(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1722,10 +1722,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): hull.apply_to(m) # this should be a feasible integer solution - m.d1.indicator_var.fix(0) - m.d2.indicator_var.fix(1) - m.d3.indicator_var.fix(0) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(False) + m.d2.indicator_var.fix(True) + m.d3.indicator_var.fix(False) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -1739,10 +1739,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d4)), 0) # and what if one of the inner disjuncts is true? - m.d1.indicator_var.fix(1) - m.d2.indicator_var.fix(0) - m.d3.indicator_var.fix(1) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(True) + m.d2.indicator_var.fix(False) + m.d3.indicator_var.fix(True) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -2398,12 +2398,12 @@ def OneCentroidPerPt(m, i): TransformationFactory('gdp.hull').apply_to(m) # fix an optimal solution - m.AssignPoint[1, 1].indicator_var.fix(1) - m.AssignPoint[1, 2].indicator_var.fix(0) - m.AssignPoint[2, 1].indicator_var.fix(0) - m.AssignPoint[2, 2].indicator_var.fix(1) - m.AssignPoint[3, 1].indicator_var.fix(1) - m.AssignPoint[3, 2].indicator_var.fix(0) + m.AssignPoint[1, 1].indicator_var.fix(True) + m.AssignPoint[1, 2].indicator_var.fix(False) + m.AssignPoint[2, 1].indicator_var.fix(False) + m.AssignPoint[2, 2].indicator_var.fix(True) + m.AssignPoint[3, 1].indicator_var.fix(True) + m.AssignPoint[3, 2].indicator_var.fix(False) m.cluster_center[1].fix(0.3059) m.cluster_center[2].fix(0.8043) From 45b61d111b9d28b3862baa39bc1d89fe8007244f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 14:51:46 -0600 Subject: [PATCH 0086/1178] Adding some new test for edge cases with nested GDP in hull --- pyomo/gdp/tests/test_hull.py | 97 ++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b7b5a11e28c..118ee4ca69a 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1832,6 +1832,103 @@ def d_r(e): cons = hull.get_disaggregation_constraint(m.x, m.d_r.inner_disj) assertExpressionsEqual(self, cons.expr, x2 == x3 + x4) + def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 10)) + m.y = Var(bounds=(-4, 5)) + m.parent1 = Disjunct() + m.parent2 = Disjunct() + m.parent2.c = Constraint(expr=m.x == 0) + m.parent_disjunction = Disjunction(expr=[m.parent1, m.parent2]) + m.child1 = Disjunct() + m.child1.c = Constraint(expr=m.x <= 8) + m.child2 = Disjunct() + m.child2.c = Constraint(expr=m.x + m.y <= 3) + m.child3 = Disjunct() + m.child3.c = Constraint(expr=m.x <= 7) + m.parent1.disjunction = Disjunction(expr=[m.child1, m.child2, m.child3]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + y_c2 = hull.get_disaggregated_var(m.y, m.child2) + self.assertEqual(y_c2.bounds, (-4, 5)) + other_y = hull.get_disaggregated_var(m.y, m.child1) + self.assertEqual(other_y.bounds, (-4, 5)) + other_other_y = hull.get_disaggregated_var(m.y, m.child3) + self.assertIs(other_y, other_other_y) + y_p1 = hull.get_disaggregated_var(m.y, m.parent1) + self.assertEqual(y_p1.bounds, (-4, 5)) + y_p2 = hull.get_disaggregated_var(m.y, m.parent2) + self.assertEqual(y_p2.bounds, (-4, 5)) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent1.disjunction) + # check that the disaggregated ys in the nested just sum to the original + assertExpressionsEqual(self, y_cons.expr, y_p1 == other_y + y_c2) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent_disjunction) + assertExpressionsEqual(self, y_cons.expr, m.y == y_p1 + y_p2) + + x_c1 = hull.get_disaggregated_var(m.x, m.child1) + x_c2 = hull.get_disaggregated_var(m.x, m.child2) + x_c3 = hull.get_disaggregated_var(m.x, m.child3) + x_p1 = hull.get_disaggregated_var(m.x, m.parent1) + x_p2 = hull.get_disaggregated_var(m.x, m.parent2) + x_cons_parent = hull.get_disaggregation_constraint(m.x, m.parent_disjunction) + assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) + x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) + assertExpressionsEqual(self, x_cons_child.expr, x_p1 == x_c1 + x_c2 + x_c3) + + def test_nested_with_var_that_skips_a_level(self): + m = ConcreteModel() + + m.x = Var(bounds=(-2, 9)) + m.y = Var(bounds=(-3, 8)) + + m.y1 = Disjunct() + m.y1.c1 = Constraint(expr=m.x >= 4) + m.y1.z1 = Disjunct() + m.y1.z1.c1 = Constraint(expr=m.y == 0) + m.y1.z1.w1 = Disjunct() + m.y1.z1.w1.c1 = Constraint(expr=m.x == 0) + m.y1.z1.w2 = Disjunct() + m.y1.z1.w2.c1 = Constraint(expr=m.x >= 1) + m.y1.z1.disjunction = Disjunction(expr=[m.y1.z1.w1, m.y1.z1.w2]) + m.y1.z2 = Disjunct() + m.y1.z2.c1 = Constraint(expr=m.y == 1) + m.y1.disjunction = Disjunction(expr=[m.y1.z1, m.y1.z2]) + m.y2 = Disjunct() + m.y2.c1 = Constraint(expr=m.x == 0) + m.disjunction = Disjunction(expr=[m.y1, m.y2]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + x_y1 = hull.get_disaggregated_var(m.x, m.y1) + x_y2 = hull.get_disaggregated_var(m.x, m.y2) + x_z1 = hull.get_disaggregated_var(m.x, m.y1.z1) + x_z2 = hull.get_disaggregated_var(m.x, m.y1.z2) + x_w1 = hull.get_disaggregated_var(m.x, m.y1.z1.w1) + x_w2 = hull.get_disaggregated_var(m.x, m.y1.z1.w2) + + y_z1 = hull.get_disaggregated_var(m.y, m.y1.z1) + y_z2 = hull.get_disaggregated_var(m.y, m.y1.z2) + y_y1 = hull.get_disaggregated_var(m.y, m.y1) + y_y2 = hull.get_disaggregated_var(m.y, m.y2) + + cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) + assertExpressionsEqual(self, cons.expr, x_z1 == x_w1 + x_w2) + cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) + assertExpressionsEqual(self, cons.expr, x_y1 == x_z2 + x_z1) + cons = hull.get_disaggregation_constraint(m.x, m.disjunction) + assertExpressionsEqual(self, cons.expr, m.x == x_y1 + x_y2) + + cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, + raise_exception=False) + self.assertIsNone(cons) + cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) + assertExpressionsEqual(self, cons.expr, y_y1 == y_z1 + y_z2) + cons = hull.get_disaggregation_constraint(m.y, m.disjunction) + assertExpressionsEqual(self, cons.expr, m.y == y_y2 + y_y1) + class TestSpecialCases(unittest.TestCase): def test_local_vars(self): From b741882fa52f052c3cccf4e4c2a530cbfabfa209 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 14:53:09 -0600 Subject: [PATCH 0087/1178] Adding option to not raise an exception when looking for disaggregated vars and constraints on transformed model --- pyomo/gdp/plugins/hull.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b8e2b3e3699..6086bd61ad1 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -885,7 +885,7 @@ def _add_local_var_suffix(self, disjunct): % (disjunct.getname(fully_qualified=True), localSuffix.ctype) ) - def get_disaggregated_var(self, v, disjunct): + def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ Returns the disaggregated variable corresponding to the Var v and the Disjunct disjunct. @@ -903,11 +903,13 @@ def get_disaggregated_var(self, v, disjunct): try: return transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][v] except: - logger.error( - "It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name) - ) - raise + if raise_exception: + logger.error( + "It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name) + ) + raise + return none def get_src_var(self, disaggregated_var): """ @@ -944,7 +946,8 @@ def get_src_var(self, disaggregated_var): # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction - def get_disaggregation_constraint(self, original_var, disjunction): + def get_disaggregation_constraint(self, original_var, disjunction, + raise_exception=True): """ Returns the disaggregation (re-aggregation?) constraint (which links the disaggregated variables to their original) @@ -974,12 +977,14 @@ def get_disaggregation_constraint(self, original_var, disjunction): ._disaggregationConstraintMap[original_var][disjunction] ) except: - logger.error( - "It doesn't appear that '%s' is a variable that was " - "disaggregated by Disjunction '%s'" - % (original_var.name, disjunction.name) - ) - raise + if raise_exception: + logger.error( + "It doesn't appear that '%s' is a variable that was " + "disaggregated by Disjunction '%s'" + % (original_var.name, disjunction.name) + ) + raise + return None def get_var_bounds_constraint(self, v): """ From b1bd5d5aeead1bd1d676968cc2ff6556154f8a6b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 16:18:31 -0600 Subject: [PATCH 0088/1178] Putting transformed components on parent Block always, a lot of performance improvements in the variable gathering logic --- pyomo/gdp/plugins/hull.py | 50 +++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 6086bd61ad1..fcb992ed6c7 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -262,7 +262,6 @@ def _apply_to_impl(self, instance, **kwds): t, t.index(), parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -298,9 +297,7 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData( - self, obj, index, parent_disjunct=None, root_disjunct=None - ): + def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -310,8 +307,12 @@ def _transform_disjunctionData( "Must be an XOR!" % obj.name ) + # We put *all* transformed things on the parent Block of this + # disjunction. We'll mark the disaggregated Vars as local, but beyond + # that, we actually need everything to get transformed again as we go up + # the nested hierarchy (if there is one) transBlock, xorConstraint = self._setup_transform_disjunctionData( - obj, root_disjunct + obj, root_disjunct=None ) disaggregationConstraint = transBlock.disaggregationConstraints @@ -325,7 +326,8 @@ def _transform_disjunctionData( varOrder = [] varsByDisjunct = ComponentMap() localVarsByDisjunct = ComponentMap() - include_fixed_vars = not self._config.assume_fixed_vars_permanent + disjunctsVarAppearsIn = ComponentMap() + setOfDisjunctsVarAppearsIn = ComponentMap() for disjunct in obj.disjuncts: if not disjunct.active: continue @@ -338,7 +340,7 @@ def _transform_disjunctionData( Constraint, active=True, sort=SortComponents.deterministic, - descend_into=(Block, Disjunct), + descend_into=Block, ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -348,8 +350,8 @@ def _transform_disjunctionData( # assume_fixed_vars_permanent to True in which case we will skip # them for var in EXPR.identify_variables( - cons.body, include_fixed=include_fixed_vars - ): + cons.body, include_fixed=not + self._config.assume_fixed_vars_permanent): # Note the use of a list so that we will # eventually disaggregate the vars in a # deterministic order (the order that we found @@ -358,6 +360,12 @@ def _transform_disjunctionData( if not var in varOrder_set: varOrder.append(var) varOrder_set.add(var) + disjunctsVarAppearsIn[var] = [disjunct] + setOfDisjunctsVarAppearsIn[var] = ComponentSet([disjunct]) + else: + if disjunct not in setOfDisjunctsVarAppearsIn[var]: + disjunctsVarAppearsIn[var].append(disjunct) + setOfDisjunctsVarAppearsIn[var].add(disjunct) # check for LocalVars Suffix localVarsByDisjunct = self._get_local_var_suffixes( @@ -368,7 +376,6 @@ def _transform_disjunctionData( # being local. Since we transform from leaf to root, we are implicitly # treating our own disaggregated variables as local, so they will not be # re-disaggregated. - varSet = [] varSet = {disj: [] for disj in obj.disjuncts} # Note that variables are local with respect to a Disjunct. We deal with # them here to do some error checking (if something is obviously not @@ -379,11 +386,8 @@ def _transform_disjunctionData( # localVars of a Disjunct later) localVars = ComponentMap() varsToDisaggregate = [] - disjunctsVarAppearsIn = ComponentMap() for var in varOrder: - disjuncts = disjunctsVarAppearsIn[var] = [ - d for d in varsByDisjunct if var in varsByDisjunct[d] - ] + disjuncts = disjunctsVarAppearsIn[var] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: @@ -398,8 +402,7 @@ def _transform_disjunctionData( # disjuncts is a list of length 1 elif localVarsByDisjunct.get(disjuncts[0]) is not None: if var in localVarsByDisjunct[disjuncts[0]]: - localVars_thisDisjunct = localVars.get(disjuncts[0]) - if localVars_thisDisjunct is not None: + if localVars.get(disjuncts[0]) is not None: localVars[disjuncts[0]].append(var) else: localVars[disjuncts[0]] = [var] @@ -408,7 +411,8 @@ def _transform_disjunctionData( varSet[disjuncts[0]].append(var) varsToDisaggregate.append(var) else: - # We don't even have have any local vars for this Disjunct. + # The user didn't declare any local vars for this Disjunct, so + # we know we're disaggregating it varSet[disjuncts[0]].append(var) varsToDisaggregate.append(var) @@ -497,18 +501,8 @@ def _transform_disjunctionData( ) disaggregatedExpr += disaggregatedVar - # We equate the sum of the disaggregated vars to var (the original) - # if parent_disjunct is None, else it needs to be the disaggregated - # var corresponding to var on the parent disjunct. This is the - # reason we transform from root to leaf: This constraint is now - # correct regardless of how nested something may have been. - parent_var = ( - var - if parent_disjunct is None - else self.get_disaggregated_var(var, parent_disjunct) - ) cons_idx = len(disaggregationConstraint) - disaggregationConstraint.add(cons_idx, parent_var == disaggregatedExpr) + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction From 207874428016f3e94459a148881786b05c370d32 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 16:31:01 -0600 Subject: [PATCH 0089/1178] A few more performance things --- pyomo/gdp/plugins/hull.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index fcb992ed6c7..2469ba9c93c 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -464,14 +464,15 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): (idx, 'ub'), var_free, ) - # maintain the mappings + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. for disj in obj.disjuncts: # Because we called _transform_disjunct above, we know that # if this isn't transformed it is because it was cleanly # deactivated, and we can just skip it. if ( disj._transformation_block is not None - and disj not in disjunctsVarAppearsIn[var] + and disj not in setOfDisjunctsVarAppearsIn[var] ): relaxationBlock = disj._transformation_block().parent_block() relaxationBlock._bigMConstraintMap[ @@ -488,12 +489,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): else: disaggregatedExpr = 0 for disjunct in disjunctsVarAppearsIn[var]: - if disjunct._transformation_block is None: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - continue - + # We know this Disjunct was active, so it has been transformed now. disaggregatedVar = ( disjunct._transformation_block() .parent_block() @@ -502,6 +498,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): disaggregatedExpr += disaggregatedVar cons_idx = len(disaggregationConstraint) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a From 563168f085d3ccb3d1291e0ad6afddca7a729a3f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 16:34:06 -0600 Subject: [PATCH 0090/1178] Transform from leaf to root in hull --- pyomo/gdp/plugins/hull.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 2469ba9c93c..25a0606dc1c 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -253,7 +253,10 @@ def _apply_to_impl(self, instance, **kwds): # Preprocess in order to find what disjunctive components need # transformation gdp_tree = self._get_gdp_tree_from_targets(instance, targets) - preprocessed_targets = gdp_tree.topological_sort() + # Transform from leaf to root: This is important for hull because for + # nested GDPs, we will introduce variables that need disaggregating into + # parent Disjuncts as we transform their child Disjunctions. + preprocessed_targets = gdp_tree.reverse_topological_sort() self._targets_set = set(preprocessed_targets) for t in preprocessed_targets: @@ -565,8 +568,8 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) for var in localVars: - # we don't need to disaggregated, we can use this Var, but we do - # need to set up its bounds constraints. + # we don't need to disaggregate, i.e., we can use this Var, but we + # do need to set up its bounds constraints. # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -671,24 +674,6 @@ def _get_local_var_set(self, disjunction): return local_var_set - def _warn_for_active_disjunct( - self, innerdisjunct, outerdisjunct, var_substitute_map, zero_substitute_map - ): - # We override the base class method because in hull, it might just be - # that we haven't gotten here yet. - disjuncts = ( - innerdisjunct.values() if innerdisjunct.is_indexed() else (innerdisjunct,) - ) - for disj in disjuncts: - if disj in self._targets_set: - # We're getting to this, have some patience. - continue - else: - # But if it wasn't in the targets after preprocessing, it - # doesn't belong in an active Disjunction that we are - # transforming and we should be confused. - _warn_for_active_disjunct(innerdisjunct, outerdisjunct) - def _transform_constraint( self, obj, disjunct, var_substitute_map, zero_substitute_map ): From 35c119bf90f78e05593ae75305a1fef9ec833553 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 2 Nov 2023 14:15:33 -0600 Subject: [PATCH 0091/1178] Clarifying a lot in hull's local var suffix handling, starting to update some tests, fixing a bug in GDPTree.parent_disjunct method --- pyomo/gdp/plugins/hull.py | 94 ++++++++++++++++++------------------ pyomo/gdp/tests/test_hull.py | 21 +++++++- pyomo/gdp/util.py | 5 +- 3 files changed, 71 insertions(+), 49 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 25a0606dc1c..86bd738eb09 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -204,16 +204,16 @@ def __init__(self): super().__init__(logger) self._targets = set() - def _add_local_vars(self, block, local_var_dict): + def _collect_local_vars_from_block(self, block, local_var_dict): localVars = block.component('LocalVars') - if type(localVars) is Suffix: + if localVars is not None and localVars.ctype is Suffix: for disj, var_list in localVars.items(): - if local_var_dict.get(disj) is None: - local_var_dict[disj] = ComponentSet(var_list) - else: + if disj in local_var_dict: local_var_dict[disj].update(var_list) + else: + local_var_dict[disj] = ComponentSet(var_list) - def _get_local_var_suffixes(self, block, local_var_dict): + def _get_local_vars_from_suffixes(self, block, local_var_dict): # You can specify suffixes on any block (disjuncts included). This # method starts from a Disjunct (presumably) and checks for a LocalVar # suffixes going both up and down the tree, adding them into the @@ -222,16 +222,14 @@ def _get_local_var_suffixes(self, block, local_var_dict): # first look beneath where we are (there could be Blocks on this # disjunct) for b in block.component_data_objects( - Block, descend_into=(Block), active=True, sort=SortComponents.deterministic + Block, descend_into=Block, active=True, sort=SortComponents.deterministic ): - self._add_local_vars(b, local_var_dict) + self._collect_local_vars_from_block(b, local_var_dict) # now traverse upwards and get what's above while block is not None: - self._add_local_vars(block, local_var_dict) + self._collect_local_vars_from_block(block, local_var_dict) block = block.parent_block() - return local_var_dict - def _apply_to(self, instance, **kwds): try: self._apply_to_impl(instance, **kwds) @@ -239,7 +237,6 @@ def _apply_to(self, instance, **kwds): self._restore_state() self._transformation_blocks.clear() self._algebraic_constraints.clear() - self._targets_set = set() def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) @@ -257,14 +254,13 @@ def _apply_to_impl(self, instance, **kwds): # nested GDPs, we will introduce variables that need disaggregating into # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() - self._targets_set = set(preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: self._transform_disjunctionData( t, t.index(), - parent_disjunct=gdp_tree.parent(t), + gdp_tree.parent(t), ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -300,7 +296,7 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData(self, obj, index, parent_disjunct=None): + def _transform_disjunctionData(self, obj, index, parent_disjunct): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -371,9 +367,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): setOfDisjunctsVarAppearsIn[var].add(disjunct) # check for LocalVars Suffix - localVarsByDisjunct = self._get_local_var_suffixes( - disjunct, localVarsByDisjunct - ) + # [ESJ 11/2/23] TODO: This could be a lot more efficient if we + # centralized it. Right now we walk up the tree to the root model + # for each Disjunct, which is pretty dumb. We could get + # user-speficied suffixes once, and then we know where we will + # create ours, or we can just track what we create. + self._get_local_vars_from_suffixes(disjunct, localVarsByDisjunct) # We will disaggregate all variables that are not explicitly declared as # being local. Since we transform from leaf to root, we are implicitly @@ -387,7 +386,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # transform the Disjuncts: Values of localVarsByDisjunct are # ComponentSets, so we need this for determinism (we iterate through the # localVars of a Disjunct later) - localVars = ComponentMap() + localVars = {disj: [] for disj in obj.disjuncts} varsToDisaggregate = [] for var in varOrder: disjuncts = disjunctsVarAppearsIn[var] @@ -405,10 +404,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # disjuncts is a list of length 1 elif localVarsByDisjunct.get(disjuncts[0]) is not None: if var in localVarsByDisjunct[disjuncts[0]]: - if localVars.get(disjuncts[0]) is not None: - localVars[disjuncts[0]].append(var) - else: - localVars[disjuncts[0]] = [var] + localVars[disjuncts[0]].append(var) else: # It's not local to this Disjunct varSet[disjuncts[0]].append(var) @@ -421,7 +417,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. - local_var_set = self._get_local_var_set(obj) + print("obj: %s" % obj) + print("parent disjunct: %s" % parent_disjunct) + parent_local_var_list = self._get_local_var_list(parent_disjunct) + print("parent_local_var_list: %s" % parent_local_var_list) or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() @@ -429,11 +428,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): disjunct, transBlock, varSet[disjunct], - localVars.get(disjunct, []), - local_var_set, + localVars[disjunct], + parent_local_var_list, ) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - xorConstraint.add(index, (or_expr, rhs)) + xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) @@ -452,8 +450,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): disaggregated_var = disaggregatedVars[idx] # mark this as local because we won't re-disaggregate if this is # a nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregated_var) + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) var_free = 1 - sum( disj.indicator_var.get_associated_binary() for disj in disjunctsVarAppearsIn[var] @@ -518,7 +516,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set): + def _transform_disjunct(self, obj, transBlock, varSet, localVars, + parent_local_var_list): # We're not using the preprocessed list here, so this could be # inactive. We've already done the error checking in preprocessing, so # we just skip it here. @@ -535,6 +534,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in varSet: + print("disaggregating %s" % var) disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -547,8 +547,8 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) # mark this as local because we won't re-disaggregate if this is a # nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregatedVar) + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregatedVar) # add the bigm constraint bigmConstraint = Constraint(transBlock.lbub) @@ -568,6 +568,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) for var in localVars: + print("we knew %s was local" % var) # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. @@ -652,27 +653,23 @@ def _declare_disaggregated_var_bounds( transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint - def _get_local_var_set(self, disjunction): - # add Suffix to the relaxation block that disaggregated variables are - # local (in case this is nested in another Disjunct) - local_var_set = None - parent_disjunct = disjunction.parent_block() - while parent_disjunct is not None: - if parent_disjunct.ctype is Disjunct: - break - parent_disjunct = parent_disjunct.parent_block() + def _get_local_var_list(self, parent_disjunct): + # Add or retrieve Suffix from parent_disjunct so that, if this is + # nested, we can use it to declare that the disaggregated variables are + # local. We return the list so that we can add to it. + local_var_list = None if parent_disjunct is not None: # This limits the cases that a user is allowed to name something # (other than a Suffix) 'LocalVars' on a Disjunct. But I am assuming # that the Suffix has to be somewhere above the disjunct in the # tree, so I can't put it on a Block that I own. And if I'm coopting # something of theirs, it may as well be here. - self._add_local_var_suffix(parent_disjunct) + self._get_local_var_suffix(parent_disjunct) if parent_disjunct.LocalVars.get(parent_disjunct) is None: parent_disjunct.LocalVars[parent_disjunct] = [] - local_var_set = parent_disjunct.LocalVars[parent_disjunct] + local_var_list = parent_disjunct.LocalVars[parent_disjunct] - return local_var_set + return local_var_list def _transform_constraint( self, obj, disjunct, var_substitute_map, zero_substitute_map @@ -847,7 +844,7 @@ def _transform_constraint( # deactivate now that we have transformed obj.deactivate() - def _add_local_var_suffix(self, disjunct): + def _get_local_var_suffix(self, disjunct): # If the Suffix is there, we will borrow it. If not, we make it. If it's # something else, we complain. localSuffix = disjunct.component("LocalVars") @@ -948,7 +945,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, ) try: - return ( + cons = ( transBlock() .parent_block() ._disaggregationConstraintMap[original_var][disjunction] @@ -962,6 +959,9 @@ def get_disaggregation_constraint(self, original_var, disjunction, ) raise return None + while not cons.active: + cons = self.get_transformed_constraints(cons)[0] + return cons def get_var_bounds_constraint(self, v): """ diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 118ee4ca69a..b8aa332174b 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -41,6 +41,7 @@ import pyomo.core.expr as EXPR from pyomo.core.base import constraint from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.gdp import Disjunct, Disjunction, GDP_Error import pyomo.gdp.tests.models as models @@ -1877,6 +1878,15 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) assertExpressionsEqual(self, x_cons_child.expr, x_p1 == x_c1 + x_c2 + x_c3) + def simplify_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}) + lb = cons.lower + ub = cons.upper + self.assertEqual(cons.lb, cons.ub) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) == lb + def test_nested_with_var_that_skips_a_level(self): m = ConcreteModel() @@ -1915,18 +1925,27 @@ def test_nested_with_var_that_skips_a_level(self): y_y2 = hull.get_disaggregated_var(m.y, m.y2) cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) - assertExpressionsEqual(self, cons.expr, x_z1 == x_w1 + x_w2) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + print(cons_expr) + print("") + print(x_z1 - x_w2 - x_w1 == 0) + assertExpressionsEqual(self, cons_expr, x_z1 - x_w2 - x_w1 == 0) cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, x_y1 == x_z2 + x_z1) cons = hull.get_disaggregation_constraint(m.x, m.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, m.x == x_y1 + x_y2) cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, raise_exception=False) self.assertIsNone(cons) cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, y_y1 == y_z1 + y_z2) cons = hull.get_disaggregation_constraint(m.y, m.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, m.y == y_y2 + y_y1) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index b460a3d691c..b5e74f73c38 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -169,7 +169,10 @@ def parent_disjunct(self, u): Arg: u : A node in the forest """ - return self.parent(self.parent(u)) + if isinstance(u, _DisjunctData) or u.ctype is Disjunct: + return self.parent(self.parent(u)) + else: + return self.parent(u) def root_disjunct(self, u): """Returns the highest parent Disjunct in the hierarchy, or None if From 88cae4ab976a0eda0b5ef652e9629dd13e9458b8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 2 Nov 2023 16:45:21 -0600 Subject: [PATCH 0092/1178] Cleaning up a lot of mess using the fact that ComponentSets are ordered, simplifying how we deal with local vars significantly. --- pyomo/gdp/plugins/hull.py | 363 ++++++++++++++++++++------------------ 1 file changed, 190 insertions(+), 173 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 86bd738eb09..80ac55d45fe 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -11,6 +11,8 @@ import logging +from collections import defaultdict + import pyomo.common.config as cfg from pyomo.common import deprecated from pyomo.common.collections import ComponentMap, ComponentSet @@ -39,6 +41,7 @@ Binary, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error +from pyomo.gdp.disjunct import _DisjunctData from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.gdp.transformed_disjunct import _TransformedDisjunct from pyomo.gdp.util import ( @@ -208,27 +211,51 @@ def _collect_local_vars_from_block(self, block, local_var_dict): localVars = block.component('LocalVars') if localVars is not None and localVars.ctype is Suffix: for disj, var_list in localVars.items(): - if disj in local_var_dict: - local_var_dict[disj].update(var_list) - else: - local_var_dict[disj] = ComponentSet(var_list) - - def _get_local_vars_from_suffixes(self, block, local_var_dict): - # You can specify suffixes on any block (disjuncts included). This - # method starts from a Disjunct (presumably) and checks for a LocalVar - # suffixes going both up and down the tree, adding them into the - # dictionary that is the second argument. - - # first look beneath where we are (there could be Blocks on this - # disjunct) - for b in block.component_data_objects( - Block, descend_into=Block, active=True, sort=SortComponents.deterministic - ): - self._collect_local_vars_from_block(b, local_var_dict) - # now traverse upwards and get what's above - while block is not None: - self._collect_local_vars_from_block(block, local_var_dict) - block = block.parent_block() + local_var_dict[disj].update(var_list) + + def _get_user_defined_local_vars(self, targets): + user_defined_local_vars = defaultdict(lambda: ComponentSet()) + seen_blocks = set() + # we go through the targets looking both up and down the hierarchy, but + # we cache what Blocks/Disjuncts we've already looked on so that we + # don't duplicate effort. + for t in targets: + if t.ctype is Disjunct or isinstance(t, _DisjunctData): + # first look beneath where we are (there could be Blocks on this + # disjunct) + for b in t.component_data_objects(Block, descend_into=Block, + active=True, + sort=SortComponents.deterministic + ): + if b not in seen_blocks: + self._collect_local_vars_from_block(b, user_defined_local_vars) + seen_blocks.add(b) + # now look up in the tree + blk = t + while blk is not None: + if blk not in seen_blocks: + self._collect_local_vars_from_block(blk, + user_defined_local_vars) + seen_blocks.add(blk) + blk = blk.parent_block() + return user_defined_local_vars + + # def _get_local_vars_from_suffixes(self, block, local_var_dict): + # # You can specify suffixes on any block (disjuncts included). This + # # method starts from a Disjunct (presumably) and checks for a LocalVar + # # suffixes going both up and down the tree, adding them into the + # # dictionary that is the second argument. + + # # first look beneath where we are (there could be Blocks on this + # # disjunct) + # for b in block.component_data_objects( + # Block, descend_into=Block, active=True, sort=SortComponents.deterministic + # ): + # self._collect_local_vars_from_block(b, local_var_dict) + # # now traverse upwards and get what's above + # while block is not None: + # self._collect_local_vars_from_block(block, local_var_dict) + # block = block.parent_block() def _apply_to(self, instance, **kwds): try: @@ -254,6 +281,8 @@ def _apply_to_impl(self, instance, **kwds): # nested GDPs, we will introduce variables that need disaggregating into # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() + local_vars_by_disjunct = self._get_user_defined_local_vars( + preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: @@ -261,6 +290,7 @@ def _apply_to_impl(self, instance, **kwds): t, t.index(), gdp_tree.parent(t), + local_vars_by_disjunct ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -296,7 +326,8 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData(self, obj, index, parent_disjunct): + def _transform_disjunctionData(self, obj, index, parent_disjunct, + local_vars_by_disjunct): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -321,16 +352,11 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): # We first go through and collect all the variables that we # are going to disaggregate. - varOrder_set = ComponentSet() - varOrder = [] - varsByDisjunct = ComponentMap() - localVarsByDisjunct = ComponentMap() - disjunctsVarAppearsIn = ComponentMap() - setOfDisjunctsVarAppearsIn = ComponentMap() + var_order = ComponentSet() + disjuncts_var_appears_in = ComponentMap() for disjunct in obj.disjuncts: if not disjunct.active: continue - disjunctVars = varsByDisjunct[disjunct] = ComponentSet() # create the key for each disjunct now transBlock._disaggregatedVarMap['disaggregatedVar'][ disjunct @@ -351,45 +377,22 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): for var in EXPR.identify_variables( cons.body, include_fixed=not self._config.assume_fixed_vars_permanent): - # Note the use of a list so that we will - # eventually disaggregate the vars in a - # deterministic order (the order that we found - # them) - disjunctVars.add(var) - if not var in varOrder_set: - varOrder.append(var) - varOrder_set.add(var) - disjunctsVarAppearsIn[var] = [disjunct] - setOfDisjunctsVarAppearsIn[var] = ComponentSet([disjunct]) + # Note that, because ComponentSets are ordered, we will + # eventually disaggregate the vars in a deterministic order + # (the order that we found them) + if var not in var_order: + var_order.add(var) + disjuncts_var_appears_in[var] = ComponentSet([disjunct]) else: - if disjunct not in setOfDisjunctsVarAppearsIn[var]: - disjunctsVarAppearsIn[var].append(disjunct) - setOfDisjunctsVarAppearsIn[var].add(disjunct) - - # check for LocalVars Suffix - # [ESJ 11/2/23] TODO: This could be a lot more efficient if we - # centralized it. Right now we walk up the tree to the root model - # for each Disjunct, which is pretty dumb. We could get - # user-speficied suffixes once, and then we know where we will - # create ours, or we can just track what we create. - self._get_local_vars_from_suffixes(disjunct, localVarsByDisjunct) + disjuncts_var_appears_in[var].add(disjunct) # We will disaggregate all variables that are not explicitly declared as # being local. Since we transform from leaf to root, we are implicitly # treating our own disaggregated variables as local, so they will not be # re-disaggregated. - varSet = {disj: [] for disj in obj.disjuncts} - # Note that variables are local with respect to a Disjunct. We deal with - # them here to do some error checking (if something is obviously not - # local since it is used in multiple Disjuncts in this Disjunction) and - # also to get a deterministic order in which to process them when we - # transform the Disjuncts: Values of localVarsByDisjunct are - # ComponentSets, so we need this for determinism (we iterate through the - # localVars of a Disjunct later) - localVars = {disj: [] for disj in obj.disjuncts} - varsToDisaggregate = [] - for var in varOrder: - disjuncts = disjunctsVarAppearsIn[var] + vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} + for var in var_order: + disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: @@ -399,21 +402,18 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): % var.getname(fully_qualified=True) ) for disj in disjuncts: - varSet[disj].append(var) - varsToDisaggregate.append(var) - # disjuncts is a list of length 1 - elif localVarsByDisjunct.get(disjuncts[0]) is not None: - if var in localVarsByDisjunct[disjuncts[0]]: - localVars[disjuncts[0]].append(var) + vars_to_disaggregate[disj].add(var) + else: # disjuncts is a set of length 1 + disjunct = next(iter(disjuncts)) + if disjunct in local_vars_by_disjunct: + if var not in local_vars_by_disjunct[disjunct]: + # It's not declared local to this Disjunct, so we + # disaggregate + vars_to_disaggregate[disjunct].add(var) else: - # It's not local to this Disjunct - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) - else: - # The user didn't declare any local vars for this Disjunct, so - # we know we're disaggregating it - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) + # The user didn't declare any local vars for this + # Disjunct, so we know we're disaggregating it + vars_to_disaggregate[disjunct].add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. @@ -424,106 +424,111 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() - self._transform_disjunct( - disjunct, - transBlock, - varSet[disjunct], - localVars[disjunct], - parent_local_var_list, - ) + if obj.active: + self._transform_disjunct( + disjunct, + transBlock, + vars_to_disaggregate[disjunct], + local_vars_by_disjunct.get(disjunct, []), + parent_local_var_list, + local_vars_by_disjunct[parent_disjunct] + ) xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) # add the reaggregation constraints - for i, var in enumerate(varsToDisaggregate): - # There are two cases here: Either the var appeared in every - # disjunct in the disjunction, or it didn't. If it did, there's - # nothing special to do: All of the disaggregated variables have - # been created, and we can just proceed and make this constraint. If - # it didn't, we need one more disaggregated variable, correctly - # defined. And then we can make the constraint. - if len(disjunctsVarAppearsIn[var]) < len(obj.disjuncts): - # create one more disaggregated var - idx = len(disaggregatedVars) - disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction - if parent_local_var_list is not None: - parent_local_var_list.append(disaggregated_var) - var_free = 1 - sum( - disj.indicator_var.get_associated_binary() - for disj in disjunctsVarAppearsIn[var] - ) - self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, - ) - # For every Disjunct the Var does not appear in, we want to map - # that this new variable is its disaggreggated variable. - for disj in obj.disjuncts: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - if ( - disj._transformation_block is not None - and disj not in setOfDisjunctsVarAppearsIn[var] - ): - relaxationBlock = disj._transformation_block().parent_block() - relaxationBlock._bigMConstraintMap[ - disaggregated_var - ] = Reference(disaggregated_var_bounds[idx, :]) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap['disaggregatedVar'][disj][ - var - ] = disaggregated_var - - disaggregatedExpr = disaggregated_var - else: - disaggregatedExpr = 0 - for disjunct in disjunctsVarAppearsIn[var]: - # We know this Disjunct was active, so it has been transformed now. - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar - - cons_idx = len(disaggregationConstraint) - # We always aggregate to the original var. If this is nested, this - # constraint will be transformed again. - disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) - # and update the map so that we can find this later. We index by - # variable and the particular disjunction because there is a - # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + i = 0 + for disj in obj.disjuncts: + if not disj.active: + continue + for var in vars_to_disaggregate[disj]: + # There are two cases here: Either the var appeared in every + # disjunct in the disjunction, or it didn't. If it did, there's + # nothing special to do: All of the disaggregated variables have + # been created, and we can just proceed and make this constraint. If + # it didn't, we need one more disaggregated variable, correctly + # defined. And then we can make the constraint. + if len(disjuncts_var_appears_in[var]) < len(obj.disjuncts): + # create one more disaggregated var + idx = len(disaggregatedVars) + disaggregated_var = disaggregatedVars[idx] + # mark this as local because we won't re-disaggregate if this is + # a nested disjunction + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) + local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) + var_free = 1 - sum( + disj.indicator_var.get_associated_binary() + for disj in disjuncts_var_appears_in[var] + ) + self._declare_disaggregated_var_bounds( + var, + disaggregated_var, + obj, + disaggregated_var_bounds, + (idx, 'lb'), + (idx, 'ub'), + var_free, + ) + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. + for disj in obj.disjuncts: + # Because we called _transform_disjunct above, we know that + # if this isn't transformed it is because it was cleanly + # deactivated, and we can just skip it. + if ( + disj._transformation_block is not None + and disj not in disjuncts_var_appears_in[var] + ): + relaxationBlock = disj._transformation_block().\ + parent_block() + relaxationBlock._bigMConstraintMap[ + disaggregated_var + ] = Reference(disaggregated_var_bounds[idx, :]) + relaxationBlock._disaggregatedVarMap['srcVar'][ + disaggregated_var + ] = var + relaxationBlock._disaggregatedVarMap[ + 'disaggregatedVar'][disj][ + var + ] = disaggregated_var + + disaggregatedExpr = disaggregated_var + else: + disaggregatedExpr = 0 + for disjunct in disjuncts_var_appears_in[var]: + # We know this Disjunct was active, so it has been transformed now. + disaggregatedVar = ( + disjunct._transformation_block() + .parent_block() + ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] + ) + disaggregatedExpr += disaggregatedVar + + cons_idx = len(disaggregationConstraint) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) + # and update the map so that we can find this later. We index by + # variable and the particular disjunction because there is a + # different one for each disjunction + if disaggregationConstraintMap.get(var) is not None: + disaggregationConstraintMap[var][obj] = disaggregationConstraint[ + cons_idx + ] + else: + thismap = disaggregationConstraintMap[var] = ComponentMap() + thismap[obj] = disaggregationConstraint[cons_idx] + + i += 1 # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, varSet, localVars, - parent_local_var_list): - # We're not using the preprocessed list here, so this could be - # inactive. We've already done the error checking in preprocessing, so - # we just skip it here. - if not obj.active: - return - + def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, + parent_local_var_suffix, parent_disjunct_local_vars): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -533,7 +538,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, # add the disaggregated variables and their bigm constraints # to the relaxationBlock - for var in varSet: + for var in vars_to_disaggregate: print("disaggregating %s" % var) disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch @@ -545,10 +550,13 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, relaxationBlock.disaggregatedVars.add_component( disaggregatedVarName, disaggregatedVar ) - # mark this as local because we won't re-disaggregate if this is a - # nested disjunction - if parent_local_var_list is not None: - parent_local_var_list.append(disaggregatedVar) + # mark this as local via the Suffix in case this is a partial + # transformation: + if parent_local_var_suffix is not None: + parent_local_var_suffix.append(disaggregatedVar) + # Record that it's local for our own bookkeeping in case we're in a + # nested situation in *this* transformation + parent_disjunct_local_vars.add(disaggregatedVar) # add the bigm constraint bigmConstraint = Constraint(transBlock.lbub) @@ -567,7 +575,13 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, transBlock, ) - for var in localVars: + for var in local_vars: + if var in vars_to_disaggregate: + logger.warning( + "Var '%s' was declared as a local Var for Disjunct '%s', " + "but it appeared in multiple Disjuncts, so it will be " + "disaggregated." % (var.name, obj.name)) + continue print("we knew %s was local" % var) # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. @@ -604,13 +618,16 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, obj ].items() ) - zero_substitute_map.update((id(v), ZeroConstant) for v in localVars) + zero_substitute_map.update((id(v), ZeroConstant) for v in local_vars) # Transform each component within this disjunct self._transform_block_components( obj, obj, var_substitute_map, zero_substitute_map ) + # Anything that was local to this Disjunct is also local to the parent, + # and just got "promoted" up there, so to speak. + parent_disjunct_local_vars.update(local_vars) # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() From a54bb122afff2c187ef93e88ee34f53c28e010ce Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 10:43:45 -0600 Subject: [PATCH 0093/1178] Fixing a bug where we use Disjunct active status after we've transformed them, which is useless becuase we've deactivated them --- pyomo/gdp/plugins/hull.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 80ac55d45fe..35656b2ff0a 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -54,6 +54,7 @@ logger = logging.getLogger('pyomo.gdp.hull') +from pytest import set_trace @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." @@ -336,6 +337,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name ) + # collect the Disjuncts we are going to transform now because we will + # change their active status when we transform them, but still need this + # list after the fact. + active_disjuncts = [disj for disj in obj.disjuncts if disj.active] # We put *all* transformed things on the parent Block of this # disjunction. We'll mark the disaggregated Vars as local, but beyond @@ -354,9 +359,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # are going to disaggregate. var_order = ComponentSet() disjuncts_var_appears_in = ComponentMap() - for disjunct in obj.disjuncts: - if not disjunct.active: - continue + for disjunct in active_disjuncts: # create the key for each disjunct now transBlock._disaggregatedVarMap['disaggregatedVar'][ disjunct @@ -440,9 +443,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # add the reaggregation constraints i = 0 - for disj in obj.disjuncts: - if not disj.active: - continue + for disj in active_disjuncts: for var in vars_to_disaggregate[disj]: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's @@ -510,6 +511,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, cons_idx = len(disaggregationConstraint) # We always aggregate to the original var. If this is nested, this # constraint will be transformed again. + print("Adding disaggregation constraint for '%s' on Disjunction '%s' " + "to Block '%s'" % + (var, obj, disaggregationConstraint.parent_block())) disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a @@ -951,7 +955,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, disjunction: a transformed Disjunction containing original_var """ for disjunct in disjunction.disjuncts: - transBlock = disjunct._transformation_block + transBlock = disjunct.transformation_block if transBlock is not None: break if transBlock is None: @@ -963,7 +967,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, try: cons = ( - transBlock() + transBlock .parent_block() ._disaggregationConstraintMap[original_var][disjunction] ) From da7ee79045bfec48f4ba4b85b46827cb5b0c9f14 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 12:58:00 -0600 Subject: [PATCH 0094/1178] Modifying APIs for getting transformed from original to account for the fact that constraints might get transformed multiple times. --- pyomo/gdp/plugins/hull.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 35656b2ff0a..b6e8065ba67 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -1011,10 +1011,30 @@ def get_var_bounds_constraint(self, v): logger.error(msg) raise try: - return transBlock._bigMConstraintMap[v] + cons = transBlock._bigMConstraintMap[v] except: logger.error(msg) raise + transformed_cons = {key: con for key, con in cons.items()} + def is_active(cons): + return all(c.active for c in cons.values()) + while not is_active(transformed_cons): + if 'lb' in transformed_cons: + transformed_cons['lb'] = self.get_transformed_constraints( + transformed_cons['lb'])[0] + if 'ub' in transformed_cons: + transformed_cons['ub'] = self.get_transformed_constraints( + transformed_cons['ub'])[0] + return transformed_cons + + def get_transformed_constraints(self, cons): + cons = super().get_transformed_constraints(cons) + while not cons[0].active: + transformed_cons = [] + for con in cons: + transformed_cons += super().get_transformed_constraints(con) + cons = transformed_cons + return cons @TransformationFactory.register( From 7fc03e1ce63c7888aa547f86f8c678e722869693 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 12:58:16 -0600 Subject: [PATCH 0095/1178] Rewriting simple nested test --- pyomo/gdp/tests/test_hull.py | 276 ++++++++++++++++++++--------------- 1 file changed, 157 insertions(+), 119 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b8aa332174b..3ef57c73274 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1551,154 +1551,184 @@ def check_transformed_constraint(self, cons, dis, lb, ind_var): def test_transformed_model_nestedDisjuncts(self): # This test tests *everything* for a simple nested disjunction case. m = models.makeNestedDisjunctions_NestedDisjuncts() - + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var + ] + hull = TransformationFactory('gdp.hull') hull.apply_to(m) transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) - # outer xor should be on this block + # check outer xor xor = transBlock.disj_xor self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 1) - self.assertEqual(xor.upper, 1) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d2.binary_indicator_var, 1) + ct.check_obj_in_active_tree(self, xor) + assertExpressionsEqual( + self, + xor.expr, + m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 + ) self.assertIs(xor, m.disj.algebraic_constraint) self.assertIs(m.disj, hull.get_src_disjunction(xor)) - # inner xor should be on this block + # check inner xor xor = m.d1.disj2.algebraic_constraint - self.assertIs(xor.parent_block(), transBlock) - self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 0) - self.assertEqual(xor.upper, 0) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.d3.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.d4.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, -1) self.assertIs(m.d1.disj2, hull.get_src_disjunction(xor)) - - # so should both disaggregation constraints - dis = transBlock.disaggregationConstraints - self.assertIsInstance(dis, Constraint) - self.assertTrue(dis.active) - self.assertEqual(len(dis), 2) - self.check_outer_disaggregation_constraint(dis[0], m.x, m.d1, m.d2) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.disj), dis[0]) - self.check_outer_disaggregation_constraint( - dis[1], m.x, m.d1.d3, m.d1.d4, rhs=hull.get_disaggregated_var(m.x, m.d1) - ) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.d1.disj2), dis[1]) - - # we should have four disjunct transformation blocks - disjBlocks = transBlock.relaxedDisjuncts - self.assertTrue(disjBlocks.active) - self.assertEqual(len(disjBlocks), 4) - - ## d1's transformation block - - disj1 = disjBlocks[0] - self.assertTrue(disj1.active) - self.assertIs(disj1, m.d1.transformation_block) - self.assertIs(m.d1, hull.get_src_disjunct(disj1)) - # check the disaggregated x is here - self.assertIsInstance(disj1.disaggregatedVars.x, Var) - self.assertEqual(disj1.disaggregatedVars.x.lb, 0) - self.assertEqual(disj1.disaggregatedVars.x.ub, 2) - self.assertIs(disj1.disaggregatedVars.x, hull.get_disaggregated_var(m.x, m.d1)) - self.assertIs(m.x, hull.get_src_var(disj1.disaggregatedVars.x)) - # check the bounds constraints - self.check_bounds_constraint_ub( - disj1.x_bounds, 2, disj1.disaggregatedVars.x, m.d1.indicator_var - ) - # transformed constraint x >= 1 - cons = hull.get_transformed_constraints(m.d1.c) - self.check_transformed_constraint( - cons, disj1.disaggregatedVars.x, 1, m.d1.indicator_var + xor = hull.get_transformed_constraints(xor) + self.assertEqual(len(xor), 1) + xor = xor[0] + ct.check_obj_in_active_tree(self, xor) + xor_expr = self.simplify_cons(xor) + assertExpressionsEqual( + self, + xor_expr, + m.d1.d3.binary_indicator_var + + m.d1.d4.binary_indicator_var - + m.d1.binary_indicator_var == 0.0 + ) + + # check disaggregation constraints + x_d3 = hull.get_disaggregated_var(m.x, m.d1.d3) + x_d4 = hull.get_disaggregated_var(m.x, m.d1.d4) + x_d1 = hull.get_disaggregated_var(m.x, m.d1) + x_d2 = hull.get_disaggregated_var(m.x, m.d2) + for x in [x_d1, x_d2, x_d3, x_d4]: + self.assertEqual(x.lb, 0) + self.assertEqual(x.ub, 2) + # Inner disjunction + cons = hull.get_disaggregation_constraint(m.x, m.d1.disj2) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + x_d1 - x_d3 - x_d4 == 0.0 + ) + # Outer disjunction + cons = hull.get_disaggregation_constraint(m.x, m.disj) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + m.x - x_d1 - x_d2 == 0.0 ) - ## d2's transformation block + ## Bound constraints - disj2 = disjBlocks[1] - self.assertTrue(disj2.active) - self.assertIs(disj2, m.d2.transformation_block) - self.assertIs(m.d2, hull.get_src_disjunct(disj2)) - # disaggregated var - x2 = disj2.disaggregatedVars.x - self.assertIsInstance(x2, Var) - self.assertEqual(x2.lb, 0) - self.assertEqual(x2.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d2), x2) - self.assertIs(hull.get_src_var(x2), m.x) - # bounds constraint - x_bounds = disj2.x_bounds - self.check_bounds_constraint_ub(x_bounds, 2, x2, m.d2.binary_indicator_var) - # transformed constraint x >= 1.1 - cons = hull.get_transformed_constraints(m.d2.c) - self.check_transformed_constraint(cons, x2, 1.1, m.d2.binary_indicator_var) - - ## d1.d3's transformation block - - disj3 = disjBlocks[2] - self.assertTrue(disj3.active) - self.assertIs(disj3, m.d1.d3.transformation_block) - self.assertIs(m.d1.d3, hull.get_src_disjunct(disj3)) - # disaggregated var - x3 = disj3.disaggregatedVars.x - self.assertIsInstance(x3, Var) - self.assertEqual(x3.lb, 0) - self.assertEqual(x3.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d3), x3) - self.assertIs(hull.get_src_var(x3), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj3.x_bounds, 2, x3, m.d1.d3.binary_indicator_var - ) - # transformed x >= 1.2 + ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) - self.check_transformed_constraint(cons, x3, 1.2, m.d1.d3.binary_indicator_var) - - ## d1.d4's transformation block - - disj4 = disjBlocks[3] - self.assertTrue(disj4.active) - self.assertIs(disj4, m.d1.d4.transformation_block) - self.assertIs(m.d1.d4, hull.get_src_disjunct(disj4)) - # disaggregated var - x4 = disj4.disaggregatedVars.x - self.assertIsInstance(x4, Var) - self.assertEqual(x4.lb, 0) - self.assertEqual(x4.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d4), x4) - self.assertIs(hull.get_src_var(x4), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj4.x_bounds, 2, x4, m.d1.d4.binary_indicator_var - ) - # transformed x >= 1.3 + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.2*m.d1.d3.binary_indicator_var - x_d3 <= 0.0 + ) + cons = hull.get_transformed_constraints(m.d1.d4.c) - self.check_transformed_constraint(cons, x4, 1.3, m.d1.d4.binary_indicator_var) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.3*m.d1.d4.binary_indicator_var - x_d4 <= 0.0 + ) + + cons = hull.get_transformed_constraints(m.d1.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.0*m.d1.binary_indicator_var - x_d1 <= 0.0 + ) + + cons = hull.get_transformed_constraints(m.d2.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.1*m.d2.binary_indicator_var - x_d2 <= 0.0 + ) + + ## Bounds constraints + cons = hull.get_var_bounds_constraint(x_d1) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d1 - 2*m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d2) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d2 - 2*m.d2.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d3) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d3 - 2*m.d1.d3.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d4) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d4 - 2*m.d1.d4.binary_indicator_var <= 0.0 + ) @unittest.skipIf(not linear_solvers, "No linear solver available") def test_solve_nested_model(self): # This is really a test that our variable references have all been moved # up correctly. m = models.makeNestedDisjunctions_NestedDisjuncts() - + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var + ] hull = TransformationFactory('gdp.hull') m_hull = hull.create_using(m) SolverFactory(linear_solvers[0]).solve(m_hull) + print("MODEL") + for cons in m_hull.component_data_objects(Constraint, active=True, + descend_into=Block): + print(cons.expr) + # check solution self.assertEqual(value(m_hull.d1.binary_indicator_var), 0) self.assertEqual(value(m_hull.d2.binary_indicator_var), 1) @@ -1887,6 +1917,14 @@ def simplify_cons(self, cons): self.assertIsNone(repn.nonlinear) return repn.to_expression(visitor) == lb + def simplify_leq_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}) + self.assertIsNone(cons.lower) + ub = cons.upper + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) <= ub + def test_nested_with_var_that_skips_a_level(self): m = ConcreteModel() From 8d1e68e533df01dda7ac4c4f9911db2ab388b7cf Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 Nov 2023 07:41:44 -0700 Subject: [PATCH 0096/1178] working on simplification contrib package --- pyomo/contrib/simplification/__init__.py | 0 pyomo/contrib/simplification/build.py | 33 +++++++++++++++++++ .../simplification/ginac_interface.cpp | 0 pyomo/contrib/simplification/simplify.py | 12 +++++++ 4 files changed, 45 insertions(+) create mode 100644 pyomo/contrib/simplification/__init__.py create mode 100644 pyomo/contrib/simplification/build.py create mode 100644 pyomo/contrib/simplification/ginac_interface.cpp create mode 100644 pyomo/contrib/simplification/simplify.py diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py new file mode 100644 index 00000000000..0b0b9828cd3 --- /dev/null +++ b/pyomo/contrib/simplification/build.py @@ -0,0 +1,33 @@ +from pybind11.setup_helpers import Pybind11Extension, build_ext +from pyomo.common.fileutils import this_file_dir +import os +from distutils.dist import Distribution +import sys + + +def build_ginac_interface(args=[]): + dname = this_file_dir() + _sources = [ + 'ginac_interface.cpp', + ] + sources = list() + for fname in _sources: + sources.append(os.path.join(dname, fname)) + extra_args = ['-std=c++11'] + ext = Pybind11Extension('ginac_interface', sources, extra_compile_args=extra_args) + + package_config = { + 'name': 'ginac_interface', + 'packages': [], + 'ext_modules': [ext], + 'cmdclass': {"build_ext": build_ext}, + } + + dist = Distribution(package_config) + dist.script_args = ['build_ext'] + args + dist.parse_command_line() + dist.run_command('build_ext') + + +if __name__ == '__main__': + build_ginac_interface(sys.argv[1:]) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py new file mode 100644 index 00000000000..70d5dfcd9ac --- /dev/null +++ b/pyomo/contrib/simplification/simplify.py @@ -0,0 +1,12 @@ +from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression +from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.numvalue import is_fixed, value + + +def simplify_with_sympy(expr: NumericExpression): + om, se = sympyify_expression(expr) + se = se.simplify() + new_expr = sympy2pyomo_expression(se, om) + if is_fixed(new_expr): + new_expr = value(new_expr) + return new_expr \ No newline at end of file From 8015d7ece05ee4608c8ddd6924218f40fd635f89 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 Nov 2023 11:39:43 -0700 Subject: [PATCH 0097/1178] working on simplification contrib package --- pyomo/contrib/simplification/build.py | 65 ++++++- .../simplification/ginac_interface.cpp | 149 ++++++++++++++++ .../simplification/ginac_interface.hpp | 165 ++++++++++++++++++ 3 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 pyomo/contrib/simplification/ginac_interface.hpp diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 0b0b9828cd3..6f16607e22b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,8 +1,12 @@ from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.fileutils import this_file_dir +from pyomo.common.fileutils import this_file_dir, find_library import os from distutils.dist import Distribution import sys +import shutil +import glob +import tempfile +from pyomo.common.envvar import PYOMO_CONFIG_DIR def build_ginac_interface(args=[]): @@ -13,14 +17,69 @@ def build_ginac_interface(args=[]): sources = list() for fname in _sources: sources.append(os.path.join(dname, fname)) + + ginac_lib = find_library('ginac') + if ginac_lib is None: + raise RuntimeError('could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable') + ginac_lib_dir = os.path.dirname(ginac_lib) + ginac_build_dir = os.path.dirname(ginac_lib_dir) + ginac_include_dir = os.path.join(ginac_build_dir, 'include') + if not os.path.exists(os.path.join(ginac_include_dir, 'ginac', 'ginac.h')): + raise RuntimeError('could not find GiNaC include directory') + + cln_lib = find_library('cln') + if cln_lib is None: + raise RuntimeError('could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable') + cln_lib_dir = os.path.dirname(cln_lib) + cln_build_dir = os.path.dirname(cln_lib_dir) + cln_include_dir = os.path.join(cln_build_dir, 'include') + if not os.path.exists(os.path.join(cln_include_dir, 'cln', 'cln.h')): + raise RuntimeError('could not find CLN include directory') + extra_args = ['-std=c++11'] - ext = Pybind11Extension('ginac_interface', sources, extra_compile_args=extra_args) + ext = Pybind11Extension( + 'ginac_interface', + sources=sources, + language='c++', + include_dirs=[cln_include_dir, ginac_include_dir], + library_dirs=[cln_lib_dir, ginac_lib_dir], + libraries=['cln', 'ginac'], + extra_compile_args=extra_args, + ) + + class ginac_build_ext(build_ext): + def run(self): + basedir = os.path.abspath(os.path.curdir) + if self.inplace: + tmpdir = this_file_dir() + else: + tmpdir = os.path.abspath(tempfile.mkdtemp()) + print("Building in '%s'" % tmpdir) + os.chdir(tmpdir) + try: + super(ginac_build_ext, self).run() + if not self.inplace: + library = glob.glob("build/*/ginac_interface.*")[0] + target = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + '.', + ) + if not os.path.exists(target): + os.makedirs(target) + shutil.copy(library, target) + finally: + os.chdir(basedir) + if not self.inplace: + shutil.rmtree(tmpdir, onerror=handleReadonly) package_config = { 'name': 'ginac_interface', 'packages': [], 'ext_modules': [ext], - 'cmdclass': {"build_ext": build_ext}, + 'cmdclass': {"build_ext": ginac_build_ext}, } dist = Distribution(package_config) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index e69de29bb2d..ccbc98d3586 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -0,0 +1,149 @@ +#include "ginac_interface.hpp" + +ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &leaf_map, PyomoExprTypes &expr_types) { + ex res; + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + res = numeric(expr.cast()); + break; + } + case var: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("x" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case param: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("p" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case product: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + break; + } + case sum: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + } + break; + } + case negation: { + py::list pyomo_args = expr.attr("args"); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types); + break; + } + case external_func: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("f" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case ExprType::power: { + py::list pyomo_args = expr.attr("args"); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types)); + break; + } + case division: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + break; + } + case unary_func: { + std::string function_name = expr.attr("getname")().cast(); + py::list pyomo_args = expr.attr("args"); + if (function_name == "exp") + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "log") + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "sin") + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "cos") + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "tan") + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "asin") + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "acos") + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "atan") + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "sqrt") + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else + throw py::value_error("Unrecognized expression type: " + function_name); + break; + } + case linear: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + } + break; + } + case named_expr: { + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, expr_types); + break; + } + case numeric_constant: { + res = numeric(expr.attr("value").cast()); + break; + } + case pyomo_unit: { + res = numeric(1.0); + break; + } + case unary_abs: { + py::list pyomo_args = expr.attr("args"); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + break; + } + default: { + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } + return res; +} + +ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types) { + std::unordered_map leaf_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, expr_types); + return res; +} + + +PYBIND11_MODULE(ginac_interface, m) { + m.def("ginac_expr_from_pyomo_expr", &ginac_expr_from_pyomo_expr); + py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "ex"); + py::enum_(m, "ExprType") + .value("py_float", ExprType::py_float) + .value("var", ExprType::var) + .value("param", ExprType::param) + .value("product", ExprType::product) + .value("sum", ExprType::sum) + .value("negation", ExprType::negation) + .value("external_func", ExprType::external_func) + .value("power", ExprType::power) + .value("division", ExprType::division) + .value("unary_func", ExprType::unary_func) + .value("linear", ExprType::linear) + .value("named_expr", ExprType::named_expr) + .value("numeric_constant", ExprType::numeric_constant) + .export_values(); +} diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp new file mode 100644 index 00000000000..de77e66d0c7 --- /dev/null +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -0,0 +1,165 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define PYBIND11_DETAILED_ERROR_MESSAGES + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace GiNaC; + +enum ExprType { + py_float = 0, + var = 1, + param = 2, + product = 3, + sum = 4, + negation = 5, + external_func = 6, + power = 7, + division = 8, + unary_func = 9, + linear = 10, + named_expr = 11, + numeric_constant = 12, + pyomo_unit = 13, + unary_abs = 14 +}; + +class PyomoExprTypes { +public: + PyomoExprTypes() { + expr_type_map[int_] = py_float; + expr_type_map[float_] = py_float; + expr_type_map[np_int16] = py_float; + expr_type_map[np_int32] = py_float; + expr_type_map[np_int64] = py_float; + expr_type_map[np_longlong] = py_float; + expr_type_map[np_uint16] = py_float; + expr_type_map[np_uint32] = py_float; + expr_type_map[np_uint64] = py_float; + expr_type_map[np_ulonglong] = py_float; + expr_type_map[np_float16] = py_float; + expr_type_map[np_float32] = py_float; + expr_type_map[np_float64] = py_float; + expr_type_map[ScalarVar] = var; + expr_type_map[_GeneralVarData] = var; + expr_type_map[AutoLinkedBinaryVar] = var; + expr_type_map[ScalarParam] = param; + expr_type_map[_ParamData] = param; + expr_type_map[MonomialTermExpression] = product; + expr_type_map[ProductExpression] = product; + expr_type_map[NPV_ProductExpression] = product; + expr_type_map[SumExpression] = sum; + expr_type_map[NPV_SumExpression] = sum; + expr_type_map[NegationExpression] = negation; + expr_type_map[NPV_NegationExpression] = negation; + expr_type_map[ExternalFunctionExpression] = external_func; + expr_type_map[NPV_ExternalFunctionExpression] = external_func; + expr_type_map[PowExpression] = ExprType::power; + expr_type_map[NPV_PowExpression] = ExprType::power; + expr_type_map[DivisionExpression] = division; + expr_type_map[NPV_DivisionExpression] = division; + expr_type_map[UnaryFunctionExpression] = unary_func; + expr_type_map[NPV_UnaryFunctionExpression] = unary_func; + expr_type_map[LinearExpression] = linear; + expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[ScalarExpression] = named_expr; + expr_type_map[Integral] = named_expr; + expr_type_map[ScalarIntegral] = named_expr; + expr_type_map[NumericConstant] = numeric_constant; + expr_type_map[_PyomoUnit] = pyomo_unit; + expr_type_map[AbsExpression] = unary_abs; + expr_type_map[NPV_AbsExpression] = unary_abs; + } + ~PyomoExprTypes() = default; + py::int_ ione = 1; + py::float_ fone = 1.0; + py::type int_ = py::type::of(ione); + py::type float_ = py::type::of(fone); + py::object np = py::module_::import("numpy"); + py::type np_int16 = np.attr("int16"); + py::type np_int32 = np.attr("int32"); + py::type np_int64 = np.attr("int64"); + py::type np_longlong = np.attr("longlong"); + py::type np_uint16 = np.attr("uint16"); + py::type np_uint32 = np.attr("uint32"); + py::type np_uint64 = np.attr("uint64"); + py::type np_ulonglong = np.attr("ulonglong"); + py::type np_float16 = np.attr("float16"); + py::type np_float32 = np.attr("float32"); + py::type np_float64 = np.attr("float64"); + py::object ScalarParam = + py::module_::import("pyomo.core.base.param").attr("ScalarParam"); + py::object _ParamData = + py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ScalarVar = + py::module_::import("pyomo.core.base.var").attr("ScalarVar"); + py::object _GeneralVarData = + py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object AutoLinkedBinaryVar = + py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); + py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); + py::object NegationExpression = numeric_expr.attr("NegationExpression"); + py::object NPV_NegationExpression = + numeric_expr.attr("NPV_NegationExpression"); + py::object ExternalFunctionExpression = + numeric_expr.attr("ExternalFunctionExpression"); + py::object NPV_ExternalFunctionExpression = + numeric_expr.attr("NPV_ExternalFunctionExpression"); + py::object PowExpression = numeric_expr.attr("PowExpression"); + py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); + py::object ProductExpression = numeric_expr.attr("ProductExpression"); + py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); + py::object MonomialTermExpression = + numeric_expr.attr("MonomialTermExpression"); + py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); + py::object NPV_DivisionExpression = + numeric_expr.attr("NPV_DivisionExpression"); + py::object SumExpression = numeric_expr.attr("SumExpression"); + py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); + py::object UnaryFunctionExpression = + numeric_expr.attr("UnaryFunctionExpression"); + py::object AbsExpression = numeric_expr.attr("AbsExpression"); + py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); + py::object NPV_UnaryFunctionExpression = + numeric_expr.attr("NPV_UnaryFunctionExpression"); + py::object LinearExpression = numeric_expr.attr("LinearExpression"); + py::object NumericConstant = + py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); + py::object expr_module = py::module_::import("pyomo.core.base.expression"); + py::object _GeneralExpressionData = + expr_module.attr("_GeneralExpressionData"); + py::object ScalarExpression = expr_module.attr("ScalarExpression"); + py::object ScalarIntegral = + py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); + py::object Integral = + py::module_::import("pyomo.dae.integral").attr("Integral"); + py::object _PyomoUnit = + py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object builtins = py::module_::import("builtins"); + py::object id = builtins.attr("id"); + py::object len = builtins.attr("len"); + py::dict expr_type_map; +}; + +ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types); From d43765c5bf9609c5f74e1a58b2ab1b32d595f101 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 8 Nov 2023 15:11:29 -0700 Subject: [PATCH 0098/1178] SAVE STATE --- pyomo/solver/results.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 8e4b6cf21a7..d7505a7ed95 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -289,11 +289,13 @@ def parse_sol_file(file, results): while i < number_of_cons: line = file.readline() constraints.append(float(line)) + i += 1 # Parse through the variable lines and capture the variables i = 0 while i < number_of_vars: line = file.readline() variables.append(float(line)) + i += 1 # Parse the exit code line and capture it exit_code = [0, 0] line = file.readline() @@ -315,30 +317,29 @@ def parse_sol_file(file, results): exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied results.solution_status = SolutionStatus.optimal - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message - else: - results.extra_info.solver_message = exit_code_message elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" results.termination_condition = TerminationCondition.locallyInfeasible results.solution_status = SolutionStatus.infeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + exit_code_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!" results.termination_condition = TerminationCondition.unbounded results.solution_status = SolutionStatus.infeasible elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + exit_code_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!") results.solver.termination_condition = TerminationCondition.iterationLimit elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " "in the solver routines!" ) - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message - else: - results.extra_info.solver_message = exit_code_message results.solver.termination_condition = TerminationCondition.error - return results - + + if results.extra_info.solver_message: + results.extra_info.solver_message += '; ' + exit_code_message + else: + results.extra_info.solver_message = exit_code_message return results def parse_yaml(): From 3d47029b9cea14660fd6612092eff1333bfb29cc Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 9 Nov 2023 18:12:26 -0700 Subject: [PATCH 0099/1178] Fixing a bug with adding multiple identical reaggregation constraints --- pyomo/gdp/plugins/hull.py | 177 ++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 93 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b6e8065ba67..7a5d752bdbb 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -355,8 +355,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints - # We first go through and collect all the variables that we - # are going to disaggregate. + # We first go through and collect all the variables that we are going to + # disaggregate. We do this in its own pass because we want to know all + # the Disjuncts that each Var appears in. var_order = ComponentSet() disjuncts_var_appears_in = ComponentMap() for disjunct in active_disjuncts: @@ -390,9 +391,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disjuncts_var_appears_in[var].add(disjunct) # We will disaggregate all variables that are not explicitly declared as - # being local. Since we transform from leaf to root, we are implicitly - # treating our own disaggregated variables as local, so they will not be - # re-disaggregated. + # being local. We have marked our own disaggregated variables as local, + # so they will not be re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} for var in var_order: disjuncts = disjuncts_var_appears_in[var] @@ -420,10 +420,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. - print("obj: %s" % obj) - print("parent disjunct: %s" % parent_disjunct) parent_local_var_list = self._get_local_var_list(parent_disjunct) - print("parent_local_var_list: %s" % parent_local_var_list) or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() @@ -443,90 +440,86 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # add the reaggregation constraints i = 0 - for disj in active_disjuncts: - for var in vars_to_disaggregate[disj]: - # There are two cases here: Either the var appeared in every - # disjunct in the disjunction, or it didn't. If it did, there's - # nothing special to do: All of the disaggregated variables have - # been created, and we can just proceed and make this constraint. If - # it didn't, we need one more disaggregated variable, correctly - # defined. And then we can make the constraint. - if len(disjuncts_var_appears_in[var]) < len(obj.disjuncts): - # create one more disaggregated var - idx = len(disaggregatedVars) - disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction - if parent_local_var_list is not None: - parent_local_var_list.append(disaggregated_var) - local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) - var_free = 1 - sum( - disj.indicator_var.get_associated_binary() - for disj in disjuncts_var_appears_in[var] - ) - self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, - ) - # For every Disjunct the Var does not appear in, we want to map - # that this new variable is its disaggreggated variable. - for disj in obj.disjuncts: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - if ( - disj._transformation_block is not None - and disj not in disjuncts_var_appears_in[var] - ): - relaxationBlock = disj._transformation_block().\ - parent_block() - relaxationBlock._bigMConstraintMap[ - disaggregated_var - ] = Reference(disaggregated_var_bounds[idx, :]) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap[ - 'disaggregatedVar'][disj][ - var - ] = disaggregated_var - - disaggregatedExpr = disaggregated_var - else: - disaggregatedExpr = 0 - for disjunct in disjuncts_var_appears_in[var]: - # We know this Disjunct was active, so it has been transformed now. - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar - - cons_idx = len(disaggregationConstraint) - # We always aggregate to the original var. If this is nested, this - # constraint will be transformed again. - print("Adding disaggregation constraint for '%s' on Disjunction '%s' " - "to Block '%s'" % - (var, obj, disaggregationConstraint.parent_block())) - disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) - # and update the map so that we can find this later. We index by - # variable and the particular disjunction because there is a - # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + for var in var_order: + # There are two cases here: Either the var appeared in every + # disjunct in the disjunction, or it didn't. If it did, there's + # nothing special to do: All of the disaggregated variables have + # been created, and we can just proceed and make this constraint. If + # it didn't, we need one more disaggregated variable, correctly + # defined. And then we can make the constraint. + if len(disjuncts_var_appears_in[var]) < len(active_disjuncts): + # create one more disaggregated var + idx = len(disaggregatedVars) + disaggregated_var = disaggregatedVars[idx] + # mark this as local because we won't re-disaggregate if this is + # a nested disjunction + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) + local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) + var_free = 1 - sum( + disj.indicator_var.get_associated_binary() + for disj in disjuncts_var_appears_in[var] + ) + self._declare_disaggregated_var_bounds( + var, + disaggregated_var, + obj, + disaggregated_var_bounds, + (idx, 'lb'), + (idx, 'ub'), + var_free, + ) + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. + for disj in active_disjuncts: + # Because we called _transform_disjunct above, we know that + # if this isn't transformed it is because it was cleanly + # deactivated, and we can just skip it. + if ( + disj._transformation_block is not None + and disj not in disjuncts_var_appears_in[var] + ): + relaxationBlock = disj._transformation_block().\ + parent_block() + relaxationBlock._bigMConstraintMap[ + disaggregated_var + ] = Reference(disaggregated_var_bounds[idx, :]) + relaxationBlock._disaggregatedVarMap['srcVar'][ + disaggregated_var + ] = var + relaxationBlock._disaggregatedVarMap[ + 'disaggregatedVar'][disj][ + var + ] = disaggregated_var + + disaggregatedExpr = disaggregated_var + else: + disaggregatedExpr = 0 + for disjunct in disjuncts_var_appears_in[var]: + # We know this Disjunct was active, so it has been transformed now. + disaggregatedVar = ( + disjunct._transformation_block() + .parent_block() + ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] + ) + disaggregatedExpr += disaggregatedVar + + cons_idx = len(disaggregationConstraint) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) + # and update the map so that we can find this later. We index by + # variable and the particular disjunction because there is a + # different one for each disjunction + if disaggregationConstraintMap.get(var) is not None: + disaggregationConstraintMap[var][obj] = disaggregationConstraint[ + cons_idx + ] + else: + thismap = disaggregationConstraintMap[var] = ComponentMap() + thismap[obj] = disaggregationConstraint[cons_idx] - i += 1 + i += 1 # deactivate for the writers obj.deactivate() @@ -543,7 +536,6 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in vars_to_disaggregate: - print("disaggregating %s" % var) disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -586,7 +578,6 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, "but it appeared in multiple Disjuncts, so it will be " "disaggregated." % (var.name, obj.name)) continue - print("we knew %s was local" % var) # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. From bb464908051c4580360098350a1828405eb5f434 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 9 Nov 2023 18:12:50 -0700 Subject: [PATCH 0100/1178] Fixing a couple nested GDP tests --- pyomo/gdp/tests/test_hull.py | 43 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 3ef57c73274..b224385bec0 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -52,6 +52,9 @@ import os from os.path import abspath, dirname, join +##DEBUG +from pytest import set_trace + currdir = dirname(abspath(__file__)) from filecmp import cmp @@ -1724,11 +1727,6 @@ def test_solve_nested_model(self): SolverFactory(linear_solvers[0]).solve(m_hull) - print("MODEL") - for cons in m_hull.component_data_objects(Constraint, active=True, - descend_into=Block): - print(cons.expr) - # check solution self.assertEqual(value(m_hull.d1.binary_indicator_var), 0) self.assertEqual(value(m_hull.d2.binary_indicator_var), 1) @@ -1892,11 +1890,14 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): self.assertEqual(y_p1.bounds, (-4, 5)) y_p2 = hull.get_disaggregated_var(m.y, m.parent2) self.assertEqual(y_p2.bounds, (-4, 5)) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent1.disjunction) # check that the disaggregated ys in the nested just sum to the original - assertExpressionsEqual(self, y_cons.expr, y_p1 == other_y + y_c2) + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, y_p1 - other_y - y_c2 == 0.0) y_cons = hull.get_disaggregation_constraint(m.y, m.parent_disjunction) - assertExpressionsEqual(self, y_cons.expr, m.y == y_p1 + y_p2) + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, m.y - y_p2 - y_p1 == 0.0) x_c1 = hull.get_disaggregated_var(m.x, m.child1) x_c2 = hull.get_disaggregated_var(m.x, m.child2) @@ -1906,7 +1907,9 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): x_cons_parent = hull.get_disaggregation_constraint(m.x, m.parent_disjunction) assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) - assertExpressionsEqual(self, x_cons_child.expr, x_p1 == x_c1 + x_c2 + x_c3) + x_cons_child_expr = self.simplify_cons(x_cons_child) + assertExpressionsEqual(self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - + x_c3 == 0.0) def simplify_cons(self, cons): visitor = LinearRepnVisitor({}, {}, {}) @@ -1934,9 +1937,9 @@ def test_nested_with_var_that_skips_a_level(self): m.y1 = Disjunct() m.y1.c1 = Constraint(expr=m.x >= 4) m.y1.z1 = Disjunct() - m.y1.z1.c1 = Constraint(expr=m.y == 0) + m.y1.z1.c1 = Constraint(expr=m.y == 2) m.y1.z1.w1 = Disjunct() - m.y1.z1.w1.c1 = Constraint(expr=m.x == 0) + m.y1.z1.w1.c1 = Constraint(expr=m.x == 3) m.y1.z1.w2 = Disjunct() m.y1.z1.w2.c1 = Constraint(expr=m.x >= 1) m.y1.z1.disjunction = Disjunction(expr=[m.y1.z1.w1, m.y1.z1.w2]) @@ -1944,7 +1947,7 @@ def test_nested_with_var_that_skips_a_level(self): m.y1.z2.c1 = Constraint(expr=m.y == 1) m.y1.disjunction = Disjunction(expr=[m.y1.z1, m.y1.z2]) m.y2 = Disjunct() - m.y2.c1 = Constraint(expr=m.x == 0) + m.y2.c1 = Constraint(expr=m.x == 4) m.disjunction = Disjunction(expr=[m.y1, m.y2]) hull = TransformationFactory('gdp.hull') @@ -1965,26 +1968,26 @@ def test_nested_with_var_that_skips_a_level(self): cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) self.assertTrue(cons.active) cons_expr = self.simplify_cons(cons) - print(cons_expr) - print("") - print(x_z1 - x_w2 - x_w1 == 0) - assertExpressionsEqual(self, cons_expr, x_z1 - x_w2 - x_w1 == 0) + assertExpressionsEqual(self, cons_expr, x_z1 - x_w1 - x_w2 == 0.0) cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, x_y1 == x_z2 + x_z1) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_y1 - x_z2 - x_z1 == 0.0) cons = hull.get_disaggregation_constraint(m.x, m.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, m.x == x_y1 + x_y2) - + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.x - x_y1 - x_y2 == 0.0) cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, raise_exception=False) self.assertIsNone(cons) cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, y_y1 == y_z1 + y_z2) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, y_y1 - y_z1 - y_z2 == 0.0) cons = hull.get_disaggregation_constraint(m.y, m.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, m.y == y_y2 + y_y1) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.y - y_y2 - y_y1 == 0.0) class TestSpecialCases(unittest.TestCase): From 60fee49b98e630baa3a1829e08bbff75e6b657ea Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 Nov 2023 13:57:36 -0700 Subject: [PATCH 0101/1178] Push changes from pair-programming --- pyomo/opt/plugins/sol.py | 1 + pyomo/solver/IPOPT.py | 4 +++- pyomo/solver/config.py | 1 + pyomo/solver/results.py | 36 ++++++++++++++---------------------- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index 6e1ca666633..255df117399 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -189,6 +189,7 @@ def _load(self, fin, res, soln, suffixes): if line == "": continue line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes if line[0] != 'suffix': # We assume this is the start of a # section like kestrel_option, which diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 875f8710b10..90c8a6d1bce 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -147,7 +147,9 @@ def solve(self, model, **kwds): results = Results() results.termination_condition = TerminationCondition.error else: - results = self._parse_solution() + # TODO: Make a context manager out of this and open the file + # to pass to the results, instead of doing this thing. + results = self._parse_solution(os.path.join(dname, model.name + '.sol'), self.info) def _parse_solution(self): # STOPPING POINT: The suggestion here is to look at the original diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index ed9008b7e1f..3f4424a8806 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -61,6 +61,7 @@ def __init__( self.load_solution: bool = self.declare( 'load_solution', ConfigValue(domain=bool, default=True) ) + self.raise_exception_on_nonoptimal_result: bool = self.declare('raise_exception_on_nonoptimal_result', ConfigValue(domain=bool, default=True)) self.symbolic_solver_labels: bool = self.declare( 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index d7505a7ed95..9aa2869b414 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -241,20 +241,20 @@ class ResultsReader: pass -def parse_sol_file(file, results): +def parse_sol_file(sol_file, nl_info): # The original reader for sol files is in pyomo.opt.plugins.sol. # Per my original complaint, it has "magic numbers" that I just don't # know how to test. It's apparently less fragile than that in APPSI. # NOTE: The Results object now also holds the solution loader, so we do # not need pass in a solution like we did previously. - if results is None: - results = Results() + # nl_info is an NLWriterInfo object that has vars, cons, etc. + results = Results() # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will # consider to be the solver message. message = [] - for line in file: + for line in sol_file: if not line: break line = line.strip() @@ -265,40 +265,32 @@ def parse_sol_file(file, results): # Once "Options" appears, we must now read the content under it. model_objects = [] if "Options" in line: - line = file.readline() + line = sol_file.readline() number_of_options = int(line) need_tolerance = False if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is number_of_options -= 2 need_tolerance = True for i in range(number_of_options + 4): - line = file.readline() + line = sol_file.readline() model_objects.append(int(line)) if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is - line = file.readline() + line = sol_file.readline() model_objects.append(float(line)) else: raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] - constraints = [] - variables = [] - # Parse through the constraint lines and capture the constraints - i = 0 - while i < number_of_cons: - line = file.readline() - constraints.append(float(line)) - i += 1 - # Parse through the variable lines and capture the variables - i = 0 - while i < number_of_vars: - line = file.readline() - variables.append(float(line)) - i += 1 + assert number_of_cons == len(nl_info.constraints) + assert number_of_vars == len(nl_info.variables) + + duals = [float(sol_file.readline()) for i in range(number_of_cons)] + variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] + # Parse the exit code line and capture it exit_code = [0, 0] - line = file.readline() + line = sol_file.readline() if line and ('objno' in line): exit_code_line = line.split() if (len(exit_code_line) != 3): From 124cdf41e8c98a233483e8578016658fec41501b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 13 Nov 2023 16:06:20 -0700 Subject: [PATCH 0102/1178] Fixing a bug where we were accidentally ignoring local vars and disaggregating them anyway --- pyomo/gdp/plugins/hull.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 7a5d752bdbb..1a76f08ff60 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -394,6 +394,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # being local. We have marked our own disaggregated variables as local, # so they will not be re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} + all_vars_to_disaggregate = ComponentSet() for var in var_order: disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct @@ -406,6 +407,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, ) for disj in disjuncts: vars_to_disaggregate[disj].add(var) + all_vars_to_disaggregate.add(var) else: # disjuncts is a set of length 1 disjunct = next(iter(disjuncts)) if disjunct in local_vars_by_disjunct: @@ -413,10 +415,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # It's not declared local to this Disjunct, so we # disaggregate vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) else: # The user didn't declare any local vars for this # Disjunct, so we know we're disaggregating it vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. @@ -440,7 +444,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # add the reaggregation constraints i = 0 - for var in var_order: + for var in all_vars_to_disaggregate: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's # nothing special to do: All of the disaggregated variables have @@ -526,6 +530,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, parent_local_var_suffix, parent_disjunct_local_vars): + print("\nTransforming '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -560,6 +565,7 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, disaggregatedVarName + "_bounds", bigmConstraint ) + print("Adding bounds constraints for '%s'" % var) self._declare_disaggregated_var_bounds( var, disaggregatedVar, @@ -590,6 +596,9 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) + print("Adding bounds constraints for local var '%s'" % var) + # TODO: This gets mapped in a place where we can't find it if we ask + # for it from the local var itself. self._declare_disaggregated_var_bounds( var, var, @@ -984,7 +993,7 @@ def get_var_bounds_constraint(self, v): Parameters ---------- - v: a Var which was created by the hull transformation as a + v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) """ From 688a3b17cfba8e8aeb93b4d03323fc0af84e4b1d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 13 Nov 2023 16:06:59 -0700 Subject: [PATCH 0103/1178] Generalizing the nested test, starting to test for not having more constraints than we expect (which currently we do) --- pyomo/gdp/tests/common_tests.py | 38 +++++++++++++++++++--- pyomo/gdp/tests/test_hull.py | 56 +++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index b475334981b..2eff67a8826 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1703,17 +1703,45 @@ def check_all_components_transformed(self, m): def check_transformation_blocks_nestedDisjunctions(self, m, transformation): disjunctionTransBlock = m.disj.algebraic_constraint.parent_block() transBlocks = disjunctionTransBlock.relaxedDisjuncts - self.assertEqual(len(transBlocks), 4) if transformation == 'bigm': + self.assertEqual(len(transBlocks), 4) self.assertIs(transBlocks[0], m.d1.d3.transformation_block) self.assertIs(transBlocks[1], m.d1.d4.transformation_block) self.assertIs(transBlocks[2], m.d1.transformation_block) self.assertIs(transBlocks[3], m.d2.transformation_block) if transformation == 'hull': - self.assertIs(transBlocks[2], m.d1.d3.transformation_block) - self.assertIs(transBlocks[3], m.d1.d4.transformation_block) - self.assertIs(transBlocks[0], m.d1.transformation_block) - self.assertIs(transBlocks[1], m.d2.transformation_block) + # This is a much more comprehensive test that doesn't depend on + # transformation Block structure, so just reuse it: + hull = TransformationFactory('gdp.hull') + d3 = hull.get_disaggregated_var(m.d1.d3.indicator_var, m.d1) + d4 = hull.get_disaggregated_var(m.d1.d4.indicator_var, m.d1) + self.check_transformed_model_nestedDisjuncts(m, d3, d4) + + # check the disaggregated indicator var bound constraints too + cons = hull.get_var_bounds_constraint(d3) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + d3 - m.d1.binary_indicator_var <= 0.0 + ) + + cons = hull.get_var_bounds_constraint(d4) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + d4 - m.d1.binary_indicator_var <= 0.0 + ) + + num_cons = len(m.component_data_objects(Constraint, + active=True, + descend_into=Block)) + self.assertEqual(num_cons, 10) def check_nested_disjunction_target(self, transformation): diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b224385bec0..cfd0feac2b2 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1564,6 +1564,36 @@ def test_transformed_model_nestedDisjuncts(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) + self.check_transformed_model_nestedDisjuncts(m, m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var) + + # Last, check that there aren't things we weren't expecting + + all_cons = list(m.component_data_objects(Constraint, active=True, + descend_into=Block)) + num_cons = len(all_cons) + # TODO: I shouldn't have d1.binary_indicator_var in the local list + # above, but I think if I do it should be ignored when it doesn't appear + # in any Disjuncts... + + # TODO: We get duplicate bounds constraints for inner disaggregated Vars + # because we declare bounds constraints for local vars every time. We + # should actually track them separately so that we don't duplicate + # bounds constraints over and over again. + for idx, cons in enumerate(all_cons): + print(idx) + print(cons.name) + print(cons.expr) + print("") + # 2 disaggregation constraints for x 0,3 + # + 4 bounds constraints for x 6,8,9,13, These are dumb: 10,14,16 + # + 2 bounds constraints for inner indicator vars 11, 12 + # + 2 exactly-one constraints 1,4 + # + 4 transformed constraints 2,5,7,15 + self.assertEqual(num_cons, 14) + + def check_transformed_model_nestedDisjuncts(self, m, d3, d4): + hull = TransformationFactory('gdp.hull') transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) @@ -1590,8 +1620,8 @@ def test_transformed_model_nestedDisjuncts(self): assertExpressionsEqual( self, xor_expr, - m.d1.d3.binary_indicator_var + - m.d1.d4.binary_indicator_var - + d3 + + d4 - m.d1.binary_indicator_var == 0.0 ) @@ -1622,8 +1652,6 @@ def test_transformed_model_nestedDisjuncts(self): m.x - x_d1 - x_d2 == 0.0 ) - ## Bound constraints - ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) self.assertEqual(len(cons), 1) @@ -1698,7 +1726,7 @@ def test_transformed_model_nestedDisjuncts(self): assertExpressionsEqual( self, cons_expr, - x_d3 - 2*m.d1.d3.binary_indicator_var <= 0.0 + x_d3 - 2*d3 <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d4) # the lb is trivial in this case, so we just have 1 @@ -1708,7 +1736,23 @@ def test_transformed_model_nestedDisjuncts(self): assertExpressionsEqual( self, cons_expr, - x_d4 - 2*m.d1.d4.binary_indicator_var <= 0.0 + x_d4 - 2*d4 <= 0.0 + ) + + # Bounds constraints for local vars + cons = hull.get_var_bounds_constraint(m.d1.d3.binary_indicator_var) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual( + self, + cons['ub'].expr, + m.d1.d3.binary_indicator_var <= m.d1.binary_indicator_var + ) + cons = hull.get_var_bounds_constraint(m.d1.d4.binary_indicator_var) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual( + self, + cons['ub'].expr, + m.d1.d4.binary_indicator_var <= m.d1.binary_indicator_var ) @unittest.skipIf(not linear_solvers, "No linear solver available") From 444e4abad71181dbccde53d9346a8f5f17f2120b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 15 Nov 2023 13:48:53 -0700 Subject: [PATCH 0104/1178] Explicitly collecting local vars so that we don't do anything silly with Vars declared as local that don't actually appear on the Disjunct, realizing that bound constraints at each level of the GDP tree matter. --- pyomo/gdp/plugins/hull.py | 22 +++++++++++++--------- pyomo/gdp/tests/test_hull.py | 15 ++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 1a76f08ff60..54332ebc666 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -334,7 +334,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, if not obj.xor: raise GDP_Error( "Cannot do hull reformulation for " - "Disjunction '%s' with OR constraint. " + "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name ) # collect the Disjuncts we are going to transform now because we will @@ -395,6 +395,11 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # so they will not be re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} all_vars_to_disaggregate = ComponentSet() + # We will ignore variables declared as local in a Disjunct that don't + # actually appear in any Constraints on that Disjunct, but in order to + # do this, we will explicitly collect the set of local_vars in this + # loop. + local_vars = defaultdict(lambda: ComponentSet()) for var in var_order: disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct @@ -411,7 +416,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, else: # disjuncts is a set of length 1 disjunct = next(iter(disjuncts)) if disjunct in local_vars_by_disjunct: - if var not in local_vars_by_disjunct[disjunct]: + if var in local_vars_by_disjunct[disjunct]: + local_vars[disjunct].add(var) + else: # It's not declared local to this Disjunct, so we # disaggregate vars_to_disaggregate[disjunct].add(var) @@ -424,6 +431,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. + + # Get the list of local variables for the parent Disjunct so that we can + # add the disaggregated variables we're about to make to it: parent_local_var_list = self._get_local_var_list(parent_disjunct) or_expr = 0 for disjunct in obj.disjuncts: @@ -433,7 +443,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disjunct, transBlock, vars_to_disaggregate[disjunct], - local_vars_by_disjunct.get(disjunct, []), + local_vars[disjunct], parent_local_var_list, local_vars_by_disjunct[parent_disjunct] ) @@ -578,12 +588,6 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, ) for var in local_vars: - if var in vars_to_disaggregate: - logger.warning( - "Var '%s' was declared as a local Var for Disjunct '%s', " - "but it appeared in multiple Disjuncts, so it will be " - "disaggregated." % (var.name, obj.name)) - continue # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index cfd0feac2b2..436367b3a89 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -406,7 +406,7 @@ def test_error_for_or(self): self.assertRaisesRegex( GDP_Error, "Cannot do hull reformulation for Disjunction " - "'disjunction' with OR constraint. Must be an XOR!*", + "'disjunction' with OR constraint. Must be an XOR!*", TransformationFactory('gdp.hull').apply_to, m, ) @@ -1572,25 +1572,18 @@ def test_transformed_model_nestedDisjuncts(self): all_cons = list(m.component_data_objects(Constraint, active=True, descend_into=Block)) num_cons = len(all_cons) - # TODO: I shouldn't have d1.binary_indicator_var in the local list - # above, but I think if I do it should be ignored when it doesn't appear - # in any Disjuncts... - - # TODO: We get duplicate bounds constraints for inner disaggregated Vars - # because we declare bounds constraints for local vars every time. We - # should actually track them separately so that we don't duplicate - # bounds constraints over and over again. + for idx, cons in enumerate(all_cons): print(idx) print(cons.name) print(cons.expr) print("") # 2 disaggregation constraints for x 0,3 - # + 4 bounds constraints for x 6,8,9,13, These are dumb: 10,14,16 + # + 6 bounds constraints for x 6,8,9,13,14,16 These are dumb: 10,14,16 # + 2 bounds constraints for inner indicator vars 11, 12 # + 2 exactly-one constraints 1,4 # + 4 transformed constraints 2,5,7,15 - self.assertEqual(num_cons, 14) + self.assertEqual(num_cons, 16) def check_transformed_model_nestedDisjuncts(self, m, d3, d4): hull = TransformationFactory('gdp.hull') From 98e8f9c8393a3981473e3ddf511a0f637f4cc8ab Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 15 Nov 2023 15:23:06 -0700 Subject: [PATCH 0105/1178] solver refactor: sol parsing --- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/solver/IPOPT.py | 243 +++++++++++++++++++++++++---- pyomo/solver/results.py | 268 +++++++++++++++++++++----------- 3 files changed, 389 insertions(+), 124 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 6a282bdeab4..ff0af67e273 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -283,7 +283,7 @@ def __call__(self, model, filename, solver_capability, io_options): return filename, symbol_map @document_kwargs_from_configdict(CONFIG) - def write(self, model, ostream, rowstream=None, colstream=None, **options): + def write(self, model, ostream, rowstream=None, colstream=None, **options) -> NLWriterInfo: """Write a model in NL format. Returns diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 90c8a6d1bce..0c61a0117bd 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -11,17 +11,25 @@ import os import subprocess +import io +import sys +from typing import Mapping from pyomo.common import Executable -from pyomo.common.config import ConfigValue +from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.tempfiles import TempfileManager from pyomo.opt import WriterFactory +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory -from pyomo.solver.results import Results, TerminationCondition, SolutionStatus -from pyomo.solver.solution import SolutionLoaderBase +from pyomo.solver.results import Results, TerminationCondition, SolutionStatus, SolFileData, parse_sol_file +from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.solver.util import SolverSystemError +from pyomo.common.tee import TeeStream +from pyomo.common.log import LogStream +from pyomo.core.expr.visitor import replace_expressions +from pyomo.core.expr.numvalue import value import logging @@ -51,12 +59,81 @@ def __init__( self.save_solver_io: bool = self.declare( 'save_solver_io', ConfigValue(domain=bool, default=False) ) + self.temp_dir: str = self.declare( + 'temp_dir', ConfigValue(domain=str, default=None) + ) + self.solver_output_logger = self.declare( + 'solver_output_logger', ConfigValue(default=logger) + ) + self.log_level = self.declare( + 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) + ) class IPOPTSolutionLoader(SolutionLoaderBase): pass +ipopt_command_line_options = { + 'acceptable_compl_inf_tol', + 'acceptable_constr_viol_tol', + 'acceptable_dual_inf_tol', + 'acceptable_tol', + 'alpha_for_y', + 'bound_frac', + 'bound_mult_init_val', + 'bound_push', + 'bound_relax_factor', + 'compl_inf_tol', + 'constr_mult_init_max', + 'constr_viol_tol', + 'diverging_iterates_tol', + 'dual_inf_tol', + 'expect_infeasible_problem', + 'file_print_level', + 'halt_on_ampl_error', + 'hessian_approximation', + 'honor_original_bounds', + 'linear_scaling_on_demand', + 'linear_solver', + 'linear_system_scaling', + 'ma27_pivtol', + 'ma27_pivtolmax', + 'ma57_pivot_order', + 'ma57_pivtol', + 'ma57_pivtolmax', + 'max_cpu_time', + 'max_iter', + 'max_refinement_steps', + 'max_soc', + 'maxit', + 'min_refinement_steps', + 'mu_init', + 'mu_max', + 'mu_oracle', + 'mu_strategy', + 'nlp_scaling_max_gradient', + 'nlp_scaling_method', + 'obj_scaling_factor', + 'option_file_name', + 'outlev', + 'output_file', + 'pardiso_matching_strategy', + 'print_level', + 'print_options_documentation', + 'print_user_options', + 'required_infeasibility_reduction', + 'slack_bound_frac', + 'slack_bound_push', + 'tol', + 'wantsol', + 'warm_start_bound_push', + 'warm_start_init_point', + 'warm_start_mult_bound_push', + 'watchdog_shortened_iter_trigger', +} + + @SolverFactory.register('ipopt', doc='The IPOPT NLP solver (new interface)') class IPOPT(SolverBase): CONFIG = IPOPTConfig() @@ -90,6 +167,32 @@ def config(self): def config(self, val): self.config = val + def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): + f = ostream + for k, val in options.items(): + if k not in ipopt_command_line_options: + f.write(str(k) + ' ' + str(val) + '\n') + + def _create_command_line(self, basename: str, config: IPOPTConfig): + cmd = [ + str(config.executable), + basename + '.nl', + '-AMPL', + 'option_file_name=' + basename + '.opt', + ] + if 'option_file_name' in config.solver_options: + raise ValueError( + 'Use IPOPT.config.temp_dir to specify the name of the options file. ' + 'Do not use IPOPT.config.solver_options["option_file_name"].' + ) + ipopt_options = dict(config.solver_options) + if config.time_limit is not None and 'max_cpu_time' not in ipopt_options: + ipopt_options['max_cpu_time'] = config.time_limit + for k, v in ipopt_options.items(): + cmd.append(str(k) + '=' + str(v)) + + return cmd + def solve(self, model, **kwds): # Check if solver is available avail = self.available() @@ -98,7 +201,7 @@ def solve(self, model, **kwds): f'Solver {self.__class__} is not available ({avail}).' ) # Update configuration options, based on keywords passed to solve - config = self.config(kwds.pop('options', {})) + config: IPOPTConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) # Get a copy of the environment to pass to the subprocess env = os.environ.copy() @@ -109,16 +212,26 @@ def solve(self, model, **kwds): ) ) # Write the model to an nl file - nl_writer = WriterFactory('nl') + nl_writer = NLWriter() # Need to add check for symbolic_solver_labels; may need to generate up # to three files for nl, row, col, if ssl == True # What we have here may or may not work with IPOPT; will find out when # we try to run it. with TempfileManager.new_context() as tempfile: - dname = tempfile.mkdtemp() - with open(os.path.join(dname, model.name + '.nl')) as nl_file, open( - os.path.join(dname, model.name + '.row') - ) as row_file, open(os.path.join(dname, model.name + '.col')) as col_file: + if config.temp_dir is None: + dname = tempfile.mkdtemp() + else: + dname = config.temp_dir + if not os.path.exists(dname): + os.mkdir(dname) + basename = os.path.join(dname, model.name) + if os.path.exists(basename + '.nl'): + raise RuntimeError(f"NL file with the same name {basename + '.nl'} already exists!") + with ( + open(basename + '.nl') as nl_file, + open(basename + '.row') as row_file, + open(basename + '.col') as col_file, + ): self.info = nl_writer.write( model, nl_file, @@ -126,32 +239,96 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + with open(basename + '.opt') as opt_file: + self._write_options_file(ostream=opt_file, options=config.solver_options) # Call IPOPT - passing the files via the subprocess - cmd = [str(config.executable), nl_file, '-AMPL'] + cmd = self._create_command_line(basename=basename, config=config) + + # this seems silly, but we have to give the subprocess slightly longer to finish than + # ipopt if config.time_limit is not None: - config.solver_options['max_cpu_time'] = config.time_limit - for key, val in config.solver_options.items(): - cmd.append(key + '=' + val) - process = subprocess.run( - cmd, timeout=config.time_limit, env=env, universal_newlines=True + timeout = config.time_limit + min(max(1.0, 0.01 * config.time_limit), 100) + else: + timeout = None + + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.tee: + ostreams.append(sys.stdout) + with TeeStream(*ostreams) as t: + process = subprocess.run( + cmd, timeout=timeout, env=env, universal_newlines=True, stdout=t.STDOUT, stderr=t.STDERR, + ) + + if process.returncode != 0: + results = Results() + results.termination_condition = TerminationCondition.error + results.solution_status = SolutionStatus.noSolution + results.solution_loader = SolutionLoader(None, None, None, None) + else: + # TODO: Make a context manager out of this and open the file + # to pass to the results, instead of doing this thing. + with open(basename + '.sol') as sol_file: + results = self._parse_solution(sol_file, self.info) + + if config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal: + raise RuntimeError('Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.') + + results.solver_name = 'ipopt' + results.solver_version = self.version() + if config.load_solution and results.solution_status == SolutionStatus.noSolution: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set config.load_solution=False to bypass this error.' ) + + if config.load_solution: + results.solution_loader.load_vars() - if process.returncode != 0: - if self.config.load_solution: - raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set config.load_solution=False and check ' - 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' - ) - results = Results() - results.termination_condition = TerminationCondition.error + if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: + if config.load_solution: + results.incumbent_objective = value(self.info.objectives[0]) else: - # TODO: Make a context manager out of this and open the file - # to pass to the results, instead of doing this thing. - results = self._parse_solution(os.path.join(dname, model.name + '.sol'), self.info) - - def _parse_solution(self): - # STOPPING POINT: The suggestion here is to look at the original - # parser, which hasn't failed yet, and rework it to be ... better? - pass + results.incumbent_objective = replace_expressions( + self.info.objectives[0].expr, + substitution_map={ + id(v): val for v, val in results.solution_loader.get_primals().items() + }, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) + + return results + + + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] + res, sol_data = parse_sol_file(sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read) + + if res.solution_status == SolutionStatus.noSolution: + res.solution_loader = SolutionLoader(None, None, None, None) + else: + rc = dict() + for v in nl_info.variables: + v_id = id(v) + rc[v_id] = (v, 0) + if v_id in sol_data.var_suffixes['ipopt_zL_out']: + zl = sol_data.var_suffixes['ipopt_zL_out'][v_id][1] + if abs(zl) > abs(rc[v_id][1]): + rc[v_id] = (v, zl) + if v_id in sol_data.var_suffixes['ipopt_zU_out']: + zu = sol_data.var_suffixes['ipopt_zU_out'][v_id][1] + if abs(zu) > abs(rc[v_id][1]): + rc[v_id] = (v, zu) + + res.solution_loader = SolutionLoader( + primals=sol_data.primals, + duals=sol_data.duals, + slacks=None, + reduced_costs=rc, + ) + + return res diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 9aa2869b414..01a56d526c6 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -10,8 +10,10 @@ # ___________________________________________________________________________ import enum -from typing import Optional, Tuple +import re +from typing import Optional, Tuple, Dict, Any, Sequence, List from datetime import datetime +import io from pyomo.common.config import ( ConfigDict, @@ -21,6 +23,10 @@ In, NonNegativeFloat, ) +from pyomo.common.collections import ComponentMap +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.objective import _ObjectiveData from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, @@ -28,6 +34,7 @@ ) from pyomo.solver.solution import SolutionLoaderBase from pyomo.solver.util import SolverSystemError +from pyomo.repn.plugins.nl_writer import NLWriterInfo class TerminationCondition(enum.Enum): @@ -199,10 +206,10 @@ def __init__( ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), ) self.incumbent_objective: Optional[float] = self.declare( - 'incumbent_objective', ConfigValue(domain=float) + 'incumbent_objective', ConfigValue(domain=float, default=None) ) self.objective_bound: Optional[float] = self.declare( - 'objective_bound', ConfigValue(domain=float) + 'objective_bound', ConfigValue(domain=float, default=None) ) self.solver_name: Optional[str] = self.declare( 'solver_name', ConfigValue(domain=str) @@ -211,7 +218,7 @@ def __init__( 'solver_version', ConfigValue(domain=tuple) ) self.iteration_count: Optional[int] = self.declare( - 'iteration_count', ConfigValue(domain=NonNegativeInt) + 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) @@ -227,6 +234,10 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) + self.solver_message: Optional[str] = self.declare( + 'solver_message', + ConfigValue(domain=str, default=None), + ) def __str__(self): s = '' @@ -241,98 +252,175 @@ class ResultsReader: pass -def parse_sol_file(sol_file, nl_info): - # The original reader for sol files is in pyomo.opt.plugins.sol. - # Per my original complaint, it has "magic numbers" that I just don't - # know how to test. It's apparently less fragile than that in APPSI. - # NOTE: The Results object now also holds the solution loader, so we do - # not need pass in a solution like we did previously. - # nl_info is an NLWriterInfo object that has vars, cons, etc. - results = Results() - - # For backwards compatibility and general safety, we will parse all - # lines until "Options" appears. Anything before "Options" we will - # consider to be the solver message. - message = [] - for line in sol_file: +class SolFileData(object): + def __init__(self) -> None: + self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() + self.duals: Dict[_ConstraintData, float] = dict() + self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() + self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() + self.problem_suffixes: Dict[str, List[Any]] = dict() + + +def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str]) -> Tuple[Results, SolFileData]: + suffixes_to_read = set(suffixes_to_read) + res = Results() + sol_data = SolFileData() + + fin = sol_file + # + # Some solvers (minto) do not write a message. We will assume + # all non-blank lines up the 'Options' line is the message. + msg = [] + while True: + line = fin.readline() if not line: + # EOF break line = line.strip() - if "Options" in line: + if line == 'Options': break - message.append(line) - message = '\n'.join(message) - # Once "Options" appears, we must now read the content under it. - model_objects = [] - if "Options" in line: - line = sol_file.readline() - number_of_options = int(line) - need_tolerance = False - if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is - number_of_options -= 2 - need_tolerance = True - for i in range(number_of_options + 4): - line = sol_file.readline() - model_objects.append(int(line)) - if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is - line = sol_file.readline() - model_objects.append(float(line)) - else: - raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") - # Identify the total number of variables and constraints - number_of_cons = model_objects[number_of_options + 1] - number_of_vars = model_objects[number_of_options + 3] - assert number_of_cons == len(nl_info.constraints) - assert number_of_vars == len(nl_info.variables) - - duals = [float(sol_file.readline()) for i in range(number_of_cons)] - variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] - - # Parse the exit code line and capture it - exit_code = [0, 0] - line = sol_file.readline() - if line and ('objno' in line): - exit_code_line = line.split() - if (len(exit_code_line) != 3): - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}.") - exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + if line: + msg.append(line) + msg = '\n'.join(msg) + z = [] + if line[:7] == "Options": + line = fin.readline() + nopts = int(line) + need_vbtol = False + if nopts > 4: # WEH - when is this true? + nopts -= 2 + need_vbtol = True + for i in range(nopts + 4): + line = fin.readline() + z += [int(line)] + if need_vbtol: # WEH - when is this true? + line = fin.readline() + z += [float(line)] else: - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") - results.extra_info.solver_message = message.strip().replace('\n', '; ') - # Not sure if next two lines are needed - # if isinstance(res.solver.message, str): - # res.solver.message = res.solver.message.replace(':', '\\x3a') - if (exit_code[1] >= 0) and (exit_code[1] <= 99): - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied - results.solution_status = SolutionStatus.optimal - elif (exit_code[1] >= 100) and (exit_code[1] <= 199): - exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied - results.solution_status = SolutionStatus.optimal - elif (exit_code[1] >= 200) and (exit_code[1] <= 299): - exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" - results.termination_condition = TerminationCondition.locallyInfeasible - results.solution_status = SolutionStatus.infeasible - elif (exit_code[1] >= 300) and (exit_code[1] <= 399): - exit_code_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!" - results.termination_condition = TerminationCondition.unbounded - results.solution_status = SolutionStatus.infeasible - elif (exit_code[1] >= 400) and (exit_code[1] <= 499): - exit_code_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " - "was stopped by a limit that you set!") - results.solver.termination_condition = TerminationCondition.iterationLimit - elif (exit_code[1] >= 500) and (exit_code[1] <= 599): - exit_code_message = ( - "FAILURE: the solver stopped by an error condition " - "in the solver routines!" - ) - results.solver.termination_condition = TerminationCondition.error + raise ValueError("no Options line found") + n = z[nopts + 3] # variables + m = z[nopts + 1] # constraints + x = [] + y = [] + i = 0 + while i < m: + line = fin.readline() + y.append(float(line)) + i += 1 + i = 0 + while i < n: + line = fin.readline() + x.append(float(line)) + i += 1 + objno = [0, 0] + line = fin.readline() + if line: # WEH - when is this true? + if line[:5] != "objno": # pragma:nocover + raise ValueError("expected 'objno', found '%s'" % (line)) + t = line.split() + if len(t) != 3: + raise ValueError( + "expected two numbers in objno line, but found '%s'" % (line) + ) + objno = [int(t[1]), int(t[2])] + res.solver_message = msg.strip().replace("\n", "; ") + res.solution_status = SolutionStatus.noSolution + res.termination_condition = TerminationCondition.unknown + if (objno[1] >= 0) and (objno[1] <= 99): + res.solution_status = SolutionStatus.optimal + res.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif (objno[1] >= 100) and (objno[1] <= 199): + res.solution_status = SolutionStatus.feasible + res.termination_condition = TerminationCondition.error + elif (objno[1] >= 200) and (objno[1] <= 299): + res.solution_status = SolutionStatus.infeasible + # TODO: this is solver dependent + res.termination_condition = TerminationCondition.locallyInfeasible + elif (objno[1] >= 300) and (objno[1] <= 399): + res.solution_status = SolutionStatus.noSolution + res.termination_condition = TerminationCondition.unbounded + elif (objno[1] >= 400) and (objno[1] <= 499): + # TODO: this is solver dependent + res.solution_status = SolutionStatus.infeasible + res.termination_condition = TerminationCondition.iterationLimit + elif (objno[1] >= 500) and (objno[1] <= 599): + res.solution_status = SolutionStatus.noSolution + res.termination_condition = TerminationCondition.error + if res.solution_status != SolutionStatus.noSolution: + for v, val in zip(nl_info.variables, x): + sol_data[id(v)] = (v, val) + if "dual" in suffixes_to_read: + for c, val in zip(nl_info.constraints, y): + sol_data[c] = val + ### Read suffixes ### + line = fin.readline() + while line: + line = line.strip() + if line == "": + continue + line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes + if line[0] != 'suffix': + # We assume this is the start of a + # section like kestrel_option, which + # comes after all suffixes. + remaining = "" + line = fin.readline() + while line: + remaining += line.strip() + "; " + line = fin.readline() + res.solver_message += remaining + break + unmasked_kind = int(line[1]) + kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob + convert_function = int + if (unmasked_kind & 4) == 4: + convert_function = float + nvalues = int(line[2]) + # namelen = int(line[3]) + # tablen = int(line[4]) + tabline = int(line[5]) + suffix_name = fin.readline().strip() + if suffix_name in suffixes_to_read: + # ignore translation of the table number to string value for now, + # this information can be obtained from the solver documentation + for n in range(tabline): + fin.readline() + if kind == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = fin.readline().split() + var_ndx = int(suf_line[0]) + var = nl_info.variables[var_ndx] + sol_data.var_suffixes[suffix_name][id(var)] = (var, convert_function(suf_line[1])) + elif kind == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = fin.readline().split() + con_ndx = int(suf_line[0]) + con = nl_info.constraints[con_ndx] + sol_data.con_suffixes[suffix_name][con] = convert_function(suf_line[1]) + elif kind == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = fin.readline().split() + obj_ndx = int(suf_line[0]) + obj = nl_info.objectives[obj_ndx] + sol_data.obj_suffixes[suffix_name][id(obj)] = (obj, convert_function(suf_line[1])) + elif kind == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(nvalues): + suf_line = fin.readline().split() + sol_data.problem_suffixes[suffix_name].append(convert_function(suf_line[1])) + else: + # do not store the suffix in the solution object + for cnt in range(nvalues): + fin.readline() + line = fin.readline() + + return res, sol_data - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message - else: - results.extra_info.solver_message = exit_code_message - return results def parse_yaml(): pass From 0ee1d7bdb227147c6c156c69c1bbe3e9a74025af Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 15 Nov 2023 16:54:26 -0700 Subject: [PATCH 0106/1178] solver refactor: sol parsing --- pyomo/repn/plugins/nl_writer.py | 6 ++--- pyomo/solver/IPOPT.py | 39 +++++++++++++++++++-------------- pyomo/solver/results.py | 6 ++--- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ff0af67e273..aec6bc036ab 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1346,9 +1346,9 @@ def write(self, model): # Generate the return information info = NLWriterInfo( - variables, - constraints, - objectives, + [i[0] for i in variables], + [i[0] for i in constraints], + [i[0] for i in objectives], sorted(amplfunc_libraries), row_labels, col_labels, diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 0c61a0117bd..f9eded4a62b 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -30,6 +30,7 @@ from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr.numvalue import value +from pyomo.core.base.suffix import Suffix import logging @@ -139,7 +140,7 @@ class IPOPT(SolverBase): CONFIG = IPOPTConfig() def __init__(self, **kwds): - self.config = self.CONFIG(kwds) + self._config = self.CONFIG(kwds) def available(self): if self.config.executable.path() is None: @@ -161,11 +162,11 @@ def version(self): @property def config(self): - return self.config + return self._config @config.setter def config(self, val): - self.config = val + self._config = val def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream @@ -228,9 +229,9 @@ def solve(self, model, **kwds): if os.path.exists(basename + '.nl'): raise RuntimeError(f"NL file with the same name {basename + '.nl'} already exists!") with ( - open(basename + '.nl') as nl_file, - open(basename + '.row') as row_file, - open(basename + '.col') as col_file, + open(basename + '.nl', 'w') as nl_file, + open(basename + '.row', 'w') as row_file, + open(basename + '.col', 'w') as col_file, ): self.info = nl_writer.write( model, @@ -239,7 +240,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) - with open(basename + '.opt') as opt_file: + with open(basename + '.opt', 'w') as opt_file: self._write_options_file(ostream=opt_file, options=config.solver_options) # Call IPOPT - passing the files via the subprocess cmd = self._create_command_line(basename=basename, config=config) @@ -263,16 +264,16 @@ def solve(self, model, **kwds): cmd, timeout=timeout, env=env, universal_newlines=True, stdout=t.STDOUT, stderr=t.STDERR, ) - if process.returncode != 0: - results = Results() - results.termination_condition = TerminationCondition.error - results.solution_status = SolutionStatus.noSolution - results.solution_loader = SolutionLoader(None, None, None, None) - else: - # TODO: Make a context manager out of this and open the file - # to pass to the results, instead of doing this thing. - with open(basename + '.sol') as sol_file: - results = self._parse_solution(sol_file, self.info) + if process.returncode != 0: + results = Results() + results.termination_condition = TerminationCondition.error + results.solution_status = SolutionStatus.noSolution + results.solution_loader = SolutionLoader(None, None, None, None) + else: + # TODO: Make a context manager out of this and open the file + # to pass to the results, instead of doing this thing. + with open(basename + '.sol', 'r') as sol_file: + results = self._parse_solution(sol_file, self.info) if config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal: raise RuntimeError('Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.') @@ -287,6 +288,10 @@ def solve(self, model, **kwds): if config.load_solution: results.solution_loader.load_vars() + if hasattr(model, 'dual') and isinstance(model.dual, Suffix) and model.dual.import_enabled(): + model.dual.update(results.solution_loader.get_duals()) + if hasattr(model, 'rc') and isinstance(model.rc, Suffix) and model.rc.import_enabled(): + model.rc.update(results.solution_loader.get_reduced_costs()) if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: if config.load_solution: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 01a56d526c6..17397b9aba0 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -112,7 +112,7 @@ class TerminationCondition(enum.Enum): unknown = 42 -class SolutionStatus(enum.IntEnum): +class SolutionStatus(enum.Enum): """ An enumeration for interpreting the result of a termination. This describes the designated status by the solver to be loaded back into the model. @@ -349,10 +349,10 @@ def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_r res.termination_condition = TerminationCondition.error if res.solution_status != SolutionStatus.noSolution: for v, val in zip(nl_info.variables, x): - sol_data[id(v)] = (v, val) + sol_data.primals[id(v)] = (v, val) if "dual" in suffixes_to_read: for c, val in zip(nl_info.constraints, y): - sol_data[c] = val + sol_data.duals[c] = val ### Read suffixes ### line = fin.readline() while line: From 97352bd197405174d35cc007e9c1afb0ab4e2496 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 07:56:10 -0700 Subject: [PATCH 0107/1178] Merge Michael's changes; apply black --- pyomo/common/formatting.py | 3 +- pyomo/contrib/appsi/solvers/highs.py | 1 - .../solvers/tests/test_persistent_solvers.py | 4 +- pyomo/repn/plugins/nl_writer.py | 4 +- pyomo/solver/IPOPT.py | 73 ++++++++++++++----- pyomo/solver/config.py | 5 +- pyomo/solver/results.py | 25 +++++-- pyomo/solver/solution.py | 21 ++++++ 8 files changed, 104 insertions(+), 32 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 5c2b329ce21..f76d16880df 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,8 +257,7 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' # line blocks - r'|(\+((-{3,})|(={3,}))\+)' # grid table + r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3d2104cdbfa..b270e4f2700 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -343,7 +343,6 @@ def set_instance(self, model): f'({self.available()}).' ) - ostreams = [ LogStream( level=self.config.log_level, logger=self.config.solver_output_logger diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 299a5bd5b7e..b50a072abbd 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1335,7 +1335,9 @@ def test_bug_1( self.assertAlmostEqual(res.incumbent_objective, 3) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_2(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_bug_2( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index aec6bc036ab..e745fabba33 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -283,7 +283,9 @@ def __call__(self, model, filename, solver_capability, io_options): return filename, symbol_map @document_kwargs_from_configdict(CONFIG) - def write(self, model, ostream, rowstream=None, colstream=None, **options) -> NLWriterInfo: + def write( + self, model, ostream, rowstream=None, colstream=None, **options + ) -> NLWriterInfo: """Write a model in NL format. Returns diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index f9eded4a62b..24896e626a5 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -23,7 +23,13 @@ from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory -from pyomo.solver.results import Results, TerminationCondition, SolutionStatus, SolFileData, parse_sol_file +from pyomo.solver.results import ( + Results, + TerminationCondition, + SolutionStatus, + SolFileData, + parse_sol_file, +) from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.solver.util import SolverSystemError from pyomo.common.tee import TeeStream @@ -65,10 +71,10 @@ def __init__( ) self.solver_output_logger = self.declare( 'solver_output_logger', ConfigValue(default=logger) - ) + ) self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) - ) + ) class IPOPTSolutionLoader(SolutionLoaderBase): @@ -227,10 +233,12 @@ def solve(self, model, **kwds): os.mkdir(dname) basename = os.path.join(dname, model.name) if os.path.exists(basename + '.nl'): - raise RuntimeError(f"NL file with the same name {basename + '.nl'} already exists!") + raise RuntimeError( + f"NL file with the same name {basename + '.nl'} already exists!" + ) with ( - open(basename + '.nl', 'w') as nl_file, - open(basename + '.row', 'w') as row_file, + open(basename + '.nl', 'w') as nl_file, + open(basename + '.row', 'w') as row_file, open(basename + '.col', 'w') as col_file, ): self.info = nl_writer.write( @@ -241,14 +249,18 @@ def solve(self, model, **kwds): symbolic_solver_labels=config.symbolic_solver_labels, ) with open(basename + '.opt', 'w') as opt_file: - self._write_options_file(ostream=opt_file, options=config.solver_options) + self._write_options_file( + ostream=opt_file, options=config.solver_options + ) # Call IPOPT - passing the files via the subprocess cmd = self._create_command_line(basename=basename, config=config) # this seems silly, but we have to give the subprocess slightly longer to finish than # ipopt if config.time_limit is not None: - timeout = config.time_limit + min(max(1.0, 0.01 * config.time_limit), 100) + timeout = config.time_limit + min( + max(1.0, 0.01 * config.time_limit), 100 + ) else: timeout = None @@ -261,7 +273,12 @@ def solve(self, model, **kwds): ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: process = subprocess.run( - cmd, timeout=timeout, env=env, universal_newlines=True, stdout=t.STDOUT, stderr=t.STDERR, + cmd, + timeout=timeout, + env=env, + universal_newlines=True, + stdout=t.STDOUT, + stderr=t.STDERR, ) if process.returncode != 0: @@ -274,23 +291,39 @@ def solve(self, model, **kwds): # to pass to the results, instead of doing this thing. with open(basename + '.sol', 'r') as sol_file: results = self._parse_solution(sol_file, self.info) - - if config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal: - raise RuntimeError('Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.') + + if ( + config.raise_exception_on_nonoptimal_result + and results.solution_status != SolutionStatus.optimal + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) results.solver_name = 'ipopt' results.solver_version = self.version() - if config.load_solution and results.solution_status == SolutionStatus.noSolution: + if ( + config.load_solution + and results.solution_status == SolutionStatus.noSolution + ): raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set config.load_solution=False to bypass this error.' ) - + if config.load_solution: results.solution_loader.load_vars() - if hasattr(model, 'dual') and isinstance(model.dual, Suffix) and model.dual.import_enabled(): + if ( + hasattr(model, 'dual') + and isinstance(model.dual, Suffix) + and model.dual.import_enabled() + ): model.dual.update(results.solution_loader.get_duals()) - if hasattr(model, 'rc') and isinstance(model.rc, Suffix) and model.rc.import_enabled(): + if ( + hasattr(model, 'rc') + and isinstance(model.rc, Suffix) + and model.rc.import_enabled() + ): model.rc.update(results.solution_loader.get_reduced_costs()) if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: @@ -300,7 +333,8 @@ def solve(self, model, **kwds): results.incumbent_objective = replace_expressions( self.info.objectives[0].expr, substitution_map={ - id(v): val for v, val in results.solution_loader.get_primals().items() + id(v): val + for v, val in results.solution_loader.get_primals().items() }, descend_into_named_expressions=True, remove_named_expressions=True, @@ -308,10 +342,11 @@ def solve(self, model, **kwds): return results - def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] - res, sol_data = parse_sol_file(sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read) + res, sol_data = parse_sol_file( + sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read + ) if res.solution_status == SolutionStatus.noSolution: res.solution_loader = SolutionLoader(None, None, None, None) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 3f4424a8806..551f59ccd9a 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -61,7 +61,10 @@ def __init__( self.load_solution: bool = self.declare( 'load_solution', ConfigValue(domain=bool, default=True) ) - self.raise_exception_on_nonoptimal_result: bool = self.declare('raise_exception_on_nonoptimal_result', ConfigValue(domain=bool, default=True)) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue(domain=bool, default=True), + ) self.symbolic_solver_labels: bool = self.declare( 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 17397b9aba0..cda8b68f715 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -235,8 +235,7 @@ def __init__( 'extra_info', ConfigDict(implicit=True) ) self.solver_message: Optional[str] = self.declare( - 'solver_message', - ConfigValue(domain=str, default=None), + 'solver_message', ConfigValue(domain=str, default=None) ) def __str__(self): @@ -262,7 +261,9 @@ def __init__(self) -> None: self.problem_suffixes: Dict[str, List[Any]] = dict() -def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str]) -> Tuple[Results, SolFileData]: +def parse_sol_file( + sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str] +) -> Tuple[Results, SolFileData]: suffixes_to_read = set(suffixes_to_read) res = Results() sol_data = SolFileData() @@ -393,26 +394,36 @@ def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_r suf_line = fin.readline().split() var_ndx = int(suf_line[0]) var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = (var, convert_function(suf_line[1])) + sol_data.var_suffixes[suffix_name][id(var)] = ( + var, + convert_function(suf_line[1]), + ) elif kind == 1: # Con sol_data.con_suffixes[suffix_name] = dict() for cnt in range(nvalues): suf_line = fin.readline().split() con_ndx = int(suf_line[0]) con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function(suf_line[1]) + sol_data.con_suffixes[suffix_name][con] = convert_function( + suf_line[1] + ) elif kind == 2: # Obj sol_data.obj_suffixes[suffix_name] = dict() for cnt in range(nvalues): suf_line = fin.readline().split() obj_ndx = int(suf_line[0]) obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = (obj, convert_function(suf_line[1])) + sol_data.obj_suffixes[suffix_name][id(obj)] = ( + obj, + convert_function(suf_line[1]), + ) elif kind == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() for cnt in range(nvalues): suf_line = fin.readline().split() - sol_data.problem_suffixes[suffix_name].append(convert_function(suf_line[1])) + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) else: # do not store the suffix in the solution object for cnt in range(nvalues): diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py index 6c4b7431746..068677ea580 100644 --- a/pyomo/solver/solution.py +++ b/pyomo/solver/solution.py @@ -17,6 +17,27 @@ from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager +# CHANGES: +# - `load` method: should just load the whole thing back into the model; load_solution = True +# - `load_variables` +# - `get_variables` +# - `get_constraints` +# - `get_objective` +# - `get_slacks` +# - `get_reduced_costs` + +# duals is how much better you could get if you weren't constrained. +# dual value of 0 means that the constraint isn't actively constraining anything. +# high dual value means that it is costing us a lot in the objective. +# can also be called "shadow price" + +# bounds on variables are implied constraints. +# getting a dual on the bound of a variable is the reduced cost. +# IPOPT calls these the bound multipliers (normally they are reduced costs, though). ZL, ZU + +# slacks are... something that I don't understand +# but they are necessary somewhere? I guess? + class SolutionLoaderBase(abc.ABC): def load_vars( From 56e8ac84e72bbd59d57e6307ef62b6dbabe09e37 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 11:43:42 -0700 Subject: [PATCH 0108/1178] working on ginac interface for simplification --- .../simplification/ginac_interface.cpp | 213 ++++++++++++++++-- .../simplification/ginac_interface.hpp | 27 ++- 2 files changed, 214 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index ccbc98d3586..9a84521ff91 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,6 +1,12 @@ #include "ginac_interface.hpp" -ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &leaf_map, PyomoExprTypes &expr_types) { +ex ginac_expr_from_pyomo_node( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { ex res; ExprType tmp_type = expr_types.expr_type_map[py::type::of(expr)].cast(); @@ -13,7 +19,21 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case var: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("x" + std::to_string(expr_id)); + std::string vname; + if (symbolic_solver_labels) { + vname = expr.attr("name").cast(); + } + else { + vname = "x" + std::to_string(expr_id); + } + py::object lb = expr.attr("lb"); + if (lb.is_none() || lb.cast() < 0) { + leaf_map[expr_id] = realsymbol(vname); + } + else { + leaf_map[expr_id] = possymbol(vname); + } + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; @@ -21,67 +41,76 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case param: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("p" + std::to_string(expr_id)); + std::string pname; + if (symbolic_solver_labels) { + pname = expr.attr("name").cast(); + } + else { + pname = "p" + std::to_string(expr_id); + } + leaf_map[expr_id] = realsymbol(pname); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; } case product: { py::list pyomo_args = expr.attr("args"); - res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case sum: { py::list pyomo_args = expr.attr("args"); for (py::handle arg : pyomo_args) { - res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); } break; } case negation: { py::list pyomo_args = expr.attr("args"); - res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case external_func: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("f" + std::to_string(expr_id)); + leaf_map[expr_id] = realsymbol("f" + std::to_string(expr_id)); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; } case ExprType::power: { py::list pyomo_args = expr.attr("args"); - res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types)); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; } case division: { py::list pyomo_args = expr.attr("args"); - res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case unary_func: { std::string function_name = expr.attr("getname")().cast(); py::list pyomo_args = expr.attr("args"); if (function_name == "exp") - res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "log") - res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "sin") - res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "cos") - res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "tan") - res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "asin") - res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "acos") - res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "atan") - res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "sqrt") - res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else throw py::value_error("Unrecognized expression type: " + function_name); break; @@ -89,12 +118,12 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case linear: { py::list pyomo_args = expr.attr("args"); for (py::handle arg : pyomo_args) { - res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); } break; } case named_expr: { - res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case numeric_constant: { @@ -107,7 +136,7 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea } case unary_abs: { py::list pyomo_args = expr.attr("args"); - res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; } default: { @@ -120,17 +149,151 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea return res; } -ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types) { +ex pyomo_expr_to_ginac_expr( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + return res; + } + +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types) { std::unordered_map leaf_map; - ex res = ginac_expr_from_pyomo_node(expr, leaf_map, expr_types); + std::unordered_map ginac_pyomo_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, true); return res; } +class GinacToPyomoVisitor +: public visitor, + public symbol::visitor, + public numeric::visitor, + public add::visitor, + public mul::visitor, + public GiNaC::power::visitor, + public function::visitor, + public basic::visitor +{ + public: + std::unordered_map *leaf_map; + std::unordered_map node_map; + PyomoExprTypes *expr_types; + + GinacToPyomoVisitor(std::unordered_map *_leaf_map, PyomoExprTypes *_expr_types) : leaf_map(_leaf_map), expr_types(_expr_types) {} + ~GinacToPyomoVisitor() = default; + + void visit(const symbol& e) { + node_map[e] = leaf_map->at(e); + } + + void visit(const numeric& e) { + double val = e.to_double(); + node_map[e] = expr_types->NumericConstant(py::cast(val)); + } + + void visit(const add& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__add__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const mul& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__mul__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const GiNaC::power& e) { + py::object arg1 = node_map[e.op(0)]; + py::object arg2 = node_map[e.op(1)]; + py::object pe = arg1.attr("__pow__")(arg2); + node_map[e] = pe; + } + + void visit(const function& e) { + py::object arg = node_map[e.op(0)]; + std::string func_type = e.get_name(); + py::object pe; + if (func_type == "exp") { + pe = expr_types->exp(arg); + } + else if (func_type == "log") { + pe = expr_types->log(arg); + } + else if (func_type == "sin") { + pe = expr_types->sin(arg); + } + else if (func_type == "cos") { + pe = expr_types->cos(arg); + } + else if (func_type == "tan") { + pe = expr_types->tan(arg); + } + else if (func_type == "asin") { + pe = expr_types->asin(arg); + } + else if (func_type == "acos") { + pe = expr_types->acos(arg); + } + else if (func_type == "atan") { + pe = expr_types->atan(arg); + } + else if (func_type == "sqrt") { + pe = expr_types->sqrt(arg); + } + else { + throw py::value_error("unrecognized unary function: " + func_type); + } + node_map[e] = pe; + } + + void visit(const basic& e) { + throw py::value_error("unrecognized ginac expression type"); + } +}; + + +ex GinacInterface::to_ginac(py::handle expr) { + return pyomo_expr_to_ginac_expr(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); +} + +py::object GinacInterface::from_ginac(ex &ge) { + GinacToPyomoVisitor v(&ginac_pyomo_map, &expr_types); + ge.traverse_postorder(v); + return v.node_map[ge]; +} + PYBIND11_MODULE(ginac_interface, m) { - m.def("ginac_expr_from_pyomo_expr", &ginac_expr_from_pyomo_expr); + m.def("pyomo_to_ginac", &pyomo_to_ginac); py::class_(m, "PyomoExprTypes").def(py::init<>()); - py::class_(m, "ex"); + py::class_(m, "ginac_expression") + .def("expand", [](ex &ge) { + // exmap m; + // ex q; + // q = ge.to_polynomial(m).normal(); + // return q.subs(m); + // return factor(ge.normal()); + return ge.expand(); + }) + .def("__str__", [](ex &ge) { + std::ostringstream stream; + stream << ge; + return stream.str(); + }); + py::class_(m, "GinacInterface") + .def(py::init()) + .def("to_ginac", &GinacInterface::to_ginac) + .def("from_ginac", &GinacInterface::from_ginac); py::enum_(m, "ExprType") .value("py_float", ExprType::py_float) .value("var", ExprType::var) diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp index de77e66d0c7..bc5b0d7b6fc 100644 --- a/pyomo/contrib/simplification/ginac_interface.hpp +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -156,10 +156,35 @@ class PyomoExprTypes { py::module_::import("pyomo.dae.integral").attr("Integral"); py::object _PyomoUnit = py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object exp = numeric_expr.attr("exp"); + py::object log = numeric_expr.attr("log"); + py::object sin = numeric_expr.attr("sin"); + py::object cos = numeric_expr.attr("cos"); + py::object tan = numeric_expr.attr("tan"); + py::object asin = numeric_expr.attr("asin"); + py::object acos = numeric_expr.attr("acos"); + py::object atan = numeric_expr.attr("atan"); + py::object sqrt = numeric_expr.attr("sqrt"); py::object builtins = py::module_::import("builtins"); py::object id = builtins.attr("id"); py::object len = builtins.attr("len"); py::dict expr_type_map; }; -ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types); +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types); + + +class GinacInterface { + public: + std::unordered_map leaf_map; + std::unordered_map ginac_pyomo_map; + PyomoExprTypes expr_types; + bool symbolic_solver_labels = false; + + GinacInterface() = default; + GinacInterface(bool _symbolic_solver_labels) : symbolic_solver_labels(_symbolic_solver_labels) {} + ~GinacInterface() = default; + + ex to_ginac(py::handle expr); + py::object from_ginac(ex &ginac_expr); +}; From 932d3d6a8a7a1cd95f2e227fe9f3a63849ad02a4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 13:07:37 -0700 Subject: [PATCH 0109/1178] ginac interface improvements --- .../simplification/ginac_interface.cpp | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 9a84521ff91..690885dc513 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,5 +1,11 @@ #include "ginac_interface.hpp" + +bool is_integer(double x) { + return std::floor(x) == x; +} + + ex ginac_expr_from_pyomo_node( py::handle expr, std::unordered_map &leaf_map, @@ -13,7 +19,13 @@ ex ginac_expr_from_pyomo_node( switch (tmp_type) { case py_float: { - res = numeric(expr.cast()); + double val = expr.cast(); + if (is_integer(val)) { + res = numeric(expr.cast()); + } + else { + res = numeric(val); + } break; } case var: { @@ -278,13 +290,9 @@ PYBIND11_MODULE(ginac_interface, m) { py::class_(m, "PyomoExprTypes").def(py::init<>()); py::class_(m, "ginac_expression") .def("expand", [](ex &ge) { - // exmap m; - // ex q; - // q = ge.to_polynomial(m).normal(); - // return q.subs(m); - // return factor(ge.normal()); return ge.expand(); }) + .def("normal", &ex::normal) .def("__str__", [](ex &ge) { std::ostringstream stream; stream << ge; From 208a5dac01ca7dab7830f70119eeb0e1ba94918e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 13:27:31 -0700 Subject: [PATCH 0110/1178] simplification interface --- pyomo/contrib/simplification/simplify.py | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 70d5dfcd9ac..1de228fb444 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,6 +1,17 @@ from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value +import logging +import warnings +try: + from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True +except: + GinacInterface = None + ginac_available = False + + +logger = logging.getLogger(__name__) def simplify_with_sympy(expr: NumericExpression): @@ -9,4 +20,26 @@ def simplify_with_sympy(expr: NumericExpression): new_expr = sympy2pyomo_expression(se, om) if is_fixed(new_expr): new_expr = value(new_expr) - return new_expr \ No newline at end of file + return new_expr + + +def simplify_with_ginac(expr: NumericExpression, ginac_interface): + gi = ginac_interface + return gi.from_ginac(gi.to_ginac(expr).normal()) + + +class Simplifier(object): + def __init__(self, supress_no_ginac_warnings: bool = False) -> None: + if ginac_available: + self.gi = GinacInterface() + self.suppress_no_ginac_warnings = supress_no_ginac_warnings + + def simplify(self, expr: NumericExpression): + if ginac_available: + return simplify_with_ginac(expr, self.gi) + else: + if not self.suppress_no_ginac_warnings: + msg = f"GiNaC does not seem to be available. Using SymPy. Note that the GiNac interface is significantly faster." + logger.warning(msg) + warnings.warn(msg) + return simplify_with_sympy(expr) From 6d945dad9c51736441586aca5fe52e8b21325804 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:27:51 -0700 Subject: [PATCH 0111/1178] Apply black --- pyomo/solver/results.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index d0ed270924c..3287cbd704b 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -291,13 +291,17 @@ def parse_sol_file( line = sol_file.readline() number_of_options = int(line) need_tolerance = False - if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is + if ( + number_of_options > 4 + ): # MRM: Entirely unclear why this is necessary, or if it even is number_of_options -= 2 need_tolerance = True for i in range(number_of_options + 4): line = sol_file.readline() model_objects.append(int(line)) - if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is + if ( + need_tolerance + ): # MRM: Entirely unclear why this is necessary, or if it even is line = sol_file.readline() model_objects.append(float(line)) else: @@ -316,11 +320,15 @@ def parse_sol_file( line = sol_file.readline() if line and ('objno' in line): exit_code_line = line.split() - if (len(exit_code_line) != 3): - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}.") + if len(exit_code_line) != 3: + raise SolverSystemError( + f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." + ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") + raise SolverSystemError( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) results.extra_info.solver_message = message.strip().replace('\n', '; ') if (exit_code[1] >= 0) and (exit_code[1] <= 99): res.solution_status = SolutionStatus.optimal @@ -336,12 +344,16 @@ def parse_sol_file( # But this was the way in the previous version - and has been fine thus far? res.termination_condition = TerminationCondition.locallyInfeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): - exit_code_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!" + exit_code_message = ( + "UNBOUNDED PROBLEM: the objective can be improved without limit!" + ) res.solution_status = SolutionStatus.noSolution res.termination_condition = TerminationCondition.unbounded elif (exit_code[1] >= 400) and (exit_code[1] <= 499): - exit_code_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " - "was stopped by a limit that you set!") + exit_code_message = ( + "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!" + ) # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? res.solution_status = SolutionStatus.infeasible From 2562d47d6f23f1bb85aee5affc64a49cf812356e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:37:26 -0700 Subject: [PATCH 0112/1178] Run black, try to fix errors --- pyomo/common/formatting.py | 3 ++- pyomo/solver/IPOPT.py | 7 +++---- pyomo/solver/results.py | 5 +---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f76d16880df..f17fa247ad0 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,7 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' + r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 1e5c1019005..5973b24b917 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -146,7 +146,7 @@ class IPOPT(SolverBase): CONFIG = IPOPTConfig() def __init__(self, **kwds): - self._config = self.CONFIG(kwds) + self.config = self.CONFIG(kwds) def available(self): if self.config.executable.path() is None: @@ -168,11 +168,11 @@ def version(self): @property def config(self): - return self._config + return self.config @config.setter def config(self, val): - self._config = val + self.config = val def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream @@ -284,7 +284,6 @@ def solve(self, model, **kwds): if process.returncode != 0: results = Results() results.termination_condition = TerminationCondition.error - results.solution_status = SolutionStatus.noSolution results.solution_loader = SolutionLoader(None, None, None, None) else: # TODO: Make a context manager out of this and open the file diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 3287cbd704b..515735acafe 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -234,9 +234,6 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - self.solver_message: Optional[str] = self.declare( - 'solver_message', ConfigValue(domain=str, default=None) - ) def __str__(self): s = '' @@ -251,7 +248,7 @@ class ResultsReader: pass -class SolFileData(object): +class SolFileData: def __init__(self) -> None: self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() self.duals: Dict[_ConstraintData, float] = dict() From 7c30a257dbc6578ab995488709e78c14bfa0af0d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:39:26 -0700 Subject: [PATCH 0113/1178] Fix IPOPT version reference in test --- pyomo/solver/tests/solvers/test_ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index afe2dbbe531..8cf046fcfef 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -39,7 +39,7 @@ def test_IPOPT_config(self): self.assertIsInstance(config.executable, ExecutableData) # Test custom initialization - solver = SolverFactory('ipopt', save_solver_io=True) + solver = SolverFactory('ipopt_v2', save_solver_io=True) self.assertTrue(solver.config.save_solver_io) self.assertFalse(solver.config.tee) From 7bf0f7f5d429d3a2f3b4be65e81beefb7e6b4fca Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:49:45 -0700 Subject: [PATCH 0114/1178] Try to fix recursive config problem --- pyomo/solver/IPOPT.py | 6 +++--- pyomo/solver/results.py | 39 +++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 5973b24b917..d0ef744aa76 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -146,7 +146,7 @@ class IPOPT(SolverBase): CONFIG = IPOPTConfig() def __init__(self, **kwds): - self.config = self.CONFIG(kwds) + self._config = self.CONFIG(kwds) def available(self): if self.config.executable.path() is None: @@ -168,11 +168,11 @@ def version(self): @property def config(self): - return self.config + return self._config @config.setter def config(self, val): - self.config = val + self._config = val def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 515735acafe..6718f954a94 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -262,13 +262,12 @@ def parse_sol_file( sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str] ) -> Tuple[Results, SolFileData]: suffixes_to_read = set(suffixes_to_read) - res = Results() sol_data = SolFileData() # # Some solvers (minto) do not write a message. We will assume # all non-blank lines up the 'Options' line is the message. - results = Results() + result = Results() # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will @@ -326,26 +325,26 @@ def parse_sol_file( raise SolverSystemError( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) - results.extra_info.solver_message = message.strip().replace('\n', '; ') + result.extra_info.solver_message = message.strip().replace('\n', '; ') if (exit_code[1] >= 0) and (exit_code[1] <= 99): - res.solution_status = SolutionStatus.optimal - res.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + result.solution_status = SolutionStatus.optimal + result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif (exit_code[1] >= 100) and (exit_code[1] <= 199): exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" - res.solution_status = SolutionStatus.feasible - res.termination_condition = TerminationCondition.error + result.solution_status = SolutionStatus.feasible + result.termination_condition = TerminationCondition.error elif (exit_code[1] >= 200) and (exit_code[1] <= 299): exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" - res.solution_status = SolutionStatus.infeasible + result.solution_status = SolutionStatus.infeasible # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? - res.termination_condition = TerminationCondition.locallyInfeasible + result.termination_condition = TerminationCondition.locallyInfeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): exit_code_message = ( "UNBOUNDED PROBLEM: the objective can be improved without limit!" ) - res.solution_status = SolutionStatus.noSolution - res.termination_condition = TerminationCondition.unbounded + result.solution_status = SolutionStatus.noSolution + result.termination_condition = TerminationCondition.unbounded elif (exit_code[1] >= 400) and (exit_code[1] <= 499): exit_code_message = ( "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " @@ -353,21 +352,21 @@ def parse_sol_file( ) # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? - res.solution_status = SolutionStatus.infeasible - res.termination_condition = TerminationCondition.iterationLimit + result.solution_status = SolutionStatus.infeasible + result.termination_condition = TerminationCondition.iterationLimit elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " "in the solver routines!" ) - res.termination_condition = TerminationCondition.error + result.termination_condition = TerminationCondition.error - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message + if result.extra_info.solver_message: + result.extra_info.solver_message += '; ' + exit_code_message else: - results.extra_info.solver_message = exit_code_message + result.extra_info.solver_message = exit_code_message - if res.solution_status != SolutionStatus.noSolution: + if result.solution_status != SolutionStatus.noSolution: for v, val in zip(nl_info.variables, variable_vals): sol_data.primals[id(v)] = (v, val) if "dual" in suffixes_to_read: @@ -390,7 +389,7 @@ def parse_sol_file( while line: remaining += line.strip() + "; " line = sol_file.readline() - res.solver_message += remaining + result.solver_message += remaining break unmasked_kind = int(line[1]) kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob @@ -449,7 +448,7 @@ def parse_sol_file( sol_file.readline() line = sol_file.readline() - return res, sol_data + return result, sol_data def parse_yaml(): From 1edc3b51715e7a734a71135f3f97798c7cd0dbf5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:03:15 -0700 Subject: [PATCH 0115/1178] Correct context manage file opening --- pyomo/solver/IPOPT.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index d0ef744aa76..79c33abcd6e 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -237,9 +237,9 @@ def solve(self, model, **kwds): f"NL file with the same name {basename + '.nl'} already exists!" ) with ( - open(basename + '.nl', 'w') as nl_file, - open(basename + '.row', 'w') as row_file, - open(basename + '.col', 'w') as col_file, + open(os.path.join(basename, '.nl'), 'w') as nl_file, + open(os.path.join(basename, '.row'), 'w') as row_file, + open(os.path.join(basename, '.col'), 'w') as col_file, ): self.info = nl_writer.write( model, From f23ed71ef662ad4553fe99b4f8e8bf9c34252de7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:24:05 -0700 Subject: [PATCH 0116/1178] More instances to be replaced --- pyomo/solver/IPOPT.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 79c33abcd6e..0029d638290 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -183,9 +183,9 @@ def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): def _create_command_line(self, basename: str, config: IPOPTConfig): cmd = [ str(config.executable), - basename + '.nl', + os.path.join(basename, '.nl'), '-AMPL', - 'option_file_name=' + basename + '.opt', + 'option_file_name=' + os.path.join(basename, '.opt'), ] if 'option_file_name' in config.solver_options: raise ValueError( @@ -232,10 +232,11 @@ def solve(self, model, **kwds): if not os.path.exists(dname): os.mkdir(dname) basename = os.path.join(dname, model.name) - if os.path.exists(basename + '.nl'): + if os.path.exists(os.path.join(basename, '.nl')): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) + print(basename, os.path.join(basename, '.nl')) with ( open(os.path.join(basename, '.nl'), 'w') as nl_file, open(os.path.join(basename, '.row'), 'w') as row_file, @@ -248,7 +249,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) - with open(basename + '.opt', 'w') as opt_file: + with open(os.path.join(basename, '.opt'), 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options ) @@ -288,7 +289,7 @@ def solve(self, model, **kwds): else: # TODO: Make a context manager out of this and open the file # to pass to the results, instead of doing this thing. - with open(basename + '.sol', 'r') as sol_file: + with open(os.path.join(basename, '.sol'), 'r') as sol_file: results = self._parse_solution(sol_file, self.info) if ( From dcd7cab7fde48e318a02a3dcca1d7252f93f6727 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:26:04 -0700 Subject: [PATCH 0117/1178] Revert - previous version was fine --- pyomo/solver/IPOPT.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 0029d638290..d0ef744aa76 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -183,9 +183,9 @@ def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): def _create_command_line(self, basename: str, config: IPOPTConfig): cmd = [ str(config.executable), - os.path.join(basename, '.nl'), + basename + '.nl', '-AMPL', - 'option_file_name=' + os.path.join(basename, '.opt'), + 'option_file_name=' + basename + '.opt', ] if 'option_file_name' in config.solver_options: raise ValueError( @@ -232,15 +232,14 @@ def solve(self, model, **kwds): if not os.path.exists(dname): os.mkdir(dname) basename = os.path.join(dname, model.name) - if os.path.exists(os.path.join(basename, '.nl')): + if os.path.exists(basename + '.nl'): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - print(basename, os.path.join(basename, '.nl')) with ( - open(os.path.join(basename, '.nl'), 'w') as nl_file, - open(os.path.join(basename, '.row'), 'w') as row_file, - open(os.path.join(basename, '.col'), 'w') as col_file, + open(basename + '.nl', 'w') as nl_file, + open(basename + '.row', 'w') as row_file, + open(basename + '.col', 'w') as col_file, ): self.info = nl_writer.write( model, @@ -249,7 +248,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) - with open(os.path.join(basename, '.opt'), 'w') as opt_file: + with open(basename + '.opt', 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options ) @@ -289,7 +288,7 @@ def solve(self, model, **kwds): else: # TODO: Make a context manager out of this and open the file # to pass to the results, instead of doing this thing. - with open(os.path.join(basename, '.sol'), 'r') as sol_file: + with open(basename + '.sol', 'r') as sol_file: results = self._parse_solution(sol_file, self.info) if ( From 883c2aba24a5ff4b10a6057aa57993bd5e031095 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:58:18 -0700 Subject: [PATCH 0118/1178] Attempt to resolve syntax issue --- pyomo/solver/IPOPT.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index d0ef744aa76..b0749cba67b 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -236,11 +236,7 @@ def solve(self, model, **kwds): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - with ( - open(basename + '.nl', 'w') as nl_file, - open(basename + '.row', 'w') as row_file, - open(basename + '.col', 'w') as col_file, - ): + with open(basename + '.nl', 'w') as nl_file, open(basename + '.row', 'w') as row_file, open(basename + '.col', 'w') as col_file: self.info = nl_writer.write( model, nl_file, From 03d3aab254c5efe5b6ecf952577a0ed3ced16f66 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 15:04:30 -0700 Subject: [PATCH 0119/1178] Apply black to context manager --- pyomo/solver/IPOPT.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index b0749cba67b..6df5914c485 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -236,7 +236,9 @@ def solve(self, model, **kwds): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - with open(basename + '.nl', 'w') as nl_file, open(basename + '.row', 'w') as row_file, open(basename + '.col', 'w') as col_file: + with open(basename + '.nl', 'w') as nl_file, open( + basename + '.row', 'w' + ) as row_file, open(basename + '.col', 'w') as col_file: self.info = nl_writer.write( model, nl_file, From 6ef89144c5b088574736e86553cb8c22d2dd9645 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 21:08:25 -0700 Subject: [PATCH 0120/1178] bugs --- pyomo/contrib/simplification/__init__.py | 1 + pyomo/contrib/simplification/simplify.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index e69de29bb2d..c09e8b8b5e5 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -0,0 +1 @@ +from .simplify import Simplifier \ No newline at end of file diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 1de228fb444..938bff6b4b9 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -31,7 +31,7 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): def __init__(self, supress_no_ginac_warnings: bool = False) -> None: if ginac_available: - self.gi = GinacInterface() + self.gi = GinacInterface(False) self.suppress_no_ginac_warnings = supress_no_ginac_warnings def simplify(self, expr: NumericExpression): From af47ef791888b6327c295a06544c4c0771e712d9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 21 Nov 2023 00:48:31 -0700 Subject: [PATCH 0121/1178] simplification tests --- .../contrib/simplification/tests/__init__.py | 0 .../tests/test_simplification.py | 62 +++++++++++++++++++ pyomo/core/expr/compare.py | 14 ++++- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/simplification/tests/__init__.py create mode 100644 pyomo/contrib/simplification/tests/test_simplification.py diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py new file mode 100644 index 00000000000..02107ba1d6c --- /dev/null +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -0,0 +1,62 @@ +from pyomo.common.unittest import TestCase +from pyomo.contrib.simplification import Simplifier +from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions +import pyomo.environ as pe +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd + + +class TestSimplification(TestCase): + def test_simplify(self): + m = pe.ConcreteModel() + x = m.x = pe.Var(bounds=(0, None)) + e = x*pe.log(x) + der1 = reverse_sd(e)[x] + der2 = reverse_sd(der1)[x] + simp = Simplifier() + der2_simp = simp.simplify(der2) + expected = x**-1.0 + assertExpressionsEqual(self, expected, der2_simp) + + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p*x**2 + p*x + p*x**2 + simp = Simplifier() + e2 = simp.simplify(e1) + exp1 = p*x**2.0*2.0 + p*x + exp2 = p*x + p*x**2.0*2.0 + self.assertTrue( + compare_expressions(e2, exp1) + or compare_expressions(e2, exp2) + or compare_expressions(e2, p*x + x**2.0*p*2.0) + or compare_expressions(e2, x**2.0*p*2.0 + p*x) + ) + + def test_mul(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2*x + simp = Simplifier() + e2 = simp.simplify(e) + expected = 2.0*x + assertExpressionsEqual(self, expected, e2) + + def test_sum(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2 + x + simp = Simplifier() + e2 = simp.simplify(e) + expected = x + 2.0 + assertExpressionsEqual(self, expected, e2) + + def test_neg(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = -pe.log(x) + simp = Simplifier() + e2 = simp.simplify(e) + expected = pe.log(x)*(-1.0) + assertExpressionsEqual(self, expected, e2) + diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..96913f1de39 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -195,7 +195,19 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): expr2, include_named_exprs=include_named_exprs ) try: - res = pn1 == pn2 + res = True + if len(pn1) != len(pn2): + res = False + if res: + for a, b in zip(pn1, pn2): + if a.__class__ is not b.__class__: + res = False + break + if a == b: + continue + else: + res = False + break except PyomoException: res = False return res From d750dfb3a6be955a4827c6d23c49afa11f1a5d22 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 10:12:14 -0700 Subject: [PATCH 0122/1178] Fix minor error in exit_code_message; change to safe_dump --- pyomo/common/config.py | 7 +++++-- pyomo/solver/results.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 1e11fbdc431..b79f2cfad25 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1037,8 +1037,11 @@ class will still create ``c`` instances that only have the single def _dump(*args, **kwds): + # TODO: Change the default behavior to no longer be YAML. + # This was a legacy decision that may no longer be the best + # decision, given changes to technology over the years. try: - from yaml import dump + from yaml import safe_dump as dump except ImportError: # dump = lambda x,**y: str(x) # YAML uses lowercase True/False @@ -1099,7 +1102,7 @@ def _value2string(prefix, value, obj): try: _data = value._data if value is obj else value if getattr(builtins, _data.__class__.__name__, None) is not None: - _str += _dump(_data, default_flow_style=True).rstrip() + _str += _dump(_data, default_flow_style=True, allow_unicode=True).rstrip() if _str.endswith("..."): _str = _str[:-3].rstrip() else: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 6718f954a94..8165e6c6310 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -326,6 +326,7 @@ def parse_sol_file( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) result.extra_info.solver_message = message.strip().replace('\n', '; ') + exit_code_message = '' if (exit_code[1] >= 0) and (exit_code[1] <= 99): result.solution_status = SolutionStatus.optimal result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied @@ -362,7 +363,8 @@ def parse_sol_file( result.termination_condition = TerminationCondition.error if result.extra_info.solver_message: - result.extra_info.solver_message += '; ' + exit_code_message + if exit_code_message: + result.extra_info.solver_message += '; ' + exit_code_message else: result.extra_info.solver_message = exit_code_message From 4b624f4ea00c012d99c3436b8b944d8945f35949 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 10:14:24 -0700 Subject: [PATCH 0123/1178] Apply black --- pyomo/common/config.py | 4 +++- pyomo/common/formatting.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index b79f2cfad25..d85df9f8286 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1102,7 +1102,9 @@ def _value2string(prefix, value, obj): try: _data = value._data if value is obj else value if getattr(builtins, _data.__class__.__name__, None) is not None: - _str += _dump(_data, default_flow_style=True, allow_unicode=True).rstrip() + _str += _dump( + _data, default_flow_style=True, allow_unicode=True + ).rstrip() if _str.endswith("..."): _str = _str[:-3].rstrip() else: diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f17fa247ad0..f76d16880df 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,8 +257,7 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' - r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections From 131a1425f083de72ce9e088b3cd2dc8b4aa919f1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 12:55:01 -0700 Subject: [PATCH 0124/1178] Remove custom SolverFactory --- pyomo/common/formatting.py | 3 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/solver/IPOPT.py | 18 +++++++----- pyomo/solver/__init__.py | 1 - pyomo/solver/base.py | 3 +- pyomo/solver/factory.py | 33 ---------------------- pyomo/solver/plugins.py | 7 +++-- pyomo/solver/results.py | 18 ++++++++---- pyomo/solver/util.py | 51 ++++++++++++++++++++++++++++------ 9 files changed, 75 insertions(+), 61 deletions(-) delete mode 100644 pyomo/solver/factory.py diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f76d16880df..f17fa247ad0 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,7 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' + r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 3a132b74395..a8f4390972f 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.solver.factory import SolverFactory +from pyomo.opt.base.solvers import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 6df5914c485..11a6a7c4cb8 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -17,21 +17,19 @@ from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager -from pyomo.opt import WriterFactory from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig -from pyomo.solver.factory import SolverFactory +from pyomo.opt.base.solvers import SolverFactory from pyomo.solver.results import ( Results, TerminationCondition, SolutionStatus, - SolFileData, parse_sol_file, ) from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader -from pyomo.solver.util import SolverSystemError from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions @@ -43,6 +41,14 @@ logger = logging.getLogger(__name__) +class SolverError(PyomoException): + """ + General exception to catch solver system errors + """ + + pass + + class IPOPTConfig(SolverConfig): def __init__( self, @@ -204,9 +210,7 @@ def solve(self, model, **kwds): # Check if solver is available avail = self.available() if not avail: - raise SolverSystemError( - f'Solver {self.__class__} is not available ({avail}).' - ) + raise SolverError(f'Solver {self.__class__} is not available ({avail}).') # Update configuration options, based on keywords passed to solve config: IPOPTConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index 1ab9f975f0b..e3eafa991cc 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -11,7 +11,6 @@ from . import base from . import config -from . import factory from . import results from . import solution from . import util diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 07f19fbb58c..8f0bd8c116f 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -76,7 +76,8 @@ def solve( timer: HierarchicalTimer An option timer for reporting timing **kwargs - Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) + Additional keyword arguments (including solver_options - passthrough + options; delivered directly to the solver (with no validation)) Returns ------- diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py deleted file mode 100644 index 84b6cf02eac..00000000000 --- a/pyomo/solver/factory.py +++ /dev/null @@ -1,33 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory -from pyomo.solver.base import LegacySolverInterface - - -class SolverFactoryClass(Factory): - def register(self, name, doc=None): - def decorator(cls): - self._cls[name] = cls - self._doc[name] = doc - - # class LegacySolver(LegacySolverInterface, cls): - # pass - - # LegacySolverFactory.register(name, doc)(LegacySolver) - - return cls - - return decorator - - -SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 5dfd4bce1eb..1dfcb6d2fe5 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -10,8 +10,11 @@ # ___________________________________________________________________________ -from .factory import SolverFactory +from pyomo.opt.base.solvers import SolverFactory +from .IPOPT import IPOPT def load(): - pass + SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( + IPOPT + ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 8165e6c6310..404977e8a1a 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ import enum -import re from typing import Optional, Tuple, Dict, Any, Sequence, List from datetime import datetime import io @@ -23,7 +22,7 @@ In, NonNegativeFloat, ) -from pyomo.common.collections import ComponentMap +from pyomo.common.errors import PyomoException from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _ConstraintData from pyomo.core.base.objective import _ObjectiveData @@ -33,10 +32,17 @@ SolverStatus as LegacySolverStatus, ) from pyomo.solver.solution import SolutionLoaderBase -from pyomo.solver.util import SolverSystemError from pyomo.repn.plugins.nl_writer import NLWriterInfo +class SolverResultsError(PyomoException): + """ + General exception to catch solver system errors + """ + + pass + + class TerminationCondition(enum.Enum): """ An Enum that enumerates all possible exit statuses for a solver call. @@ -301,7 +307,7 @@ def parse_sol_file( line = sol_file.readline() model_objects.append(float(line)) else: - raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") + raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] @@ -317,12 +323,12 @@ def parse_sol_file( if line and ('objno' in line): exit_code_line = line.split() if len(exit_code_line) != 3: - raise SolverSystemError( + raise SolverResultsError( f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise SolverSystemError( + raise SolverResultsError( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) result.extra_info.solver_message = message.strip().replace('\n', '; ') diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 79abee1b689..16d7c4d7cd4 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -20,18 +20,10 @@ from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.collections import ComponentMap -from pyomo.common.errors import PyomoException from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant from pyomo.solver.config import UpdateConfig - - -class SolverSystemError(PyomoException): - """ - General exception to catch solver system errors - """ - - pass +from pyomo.solver.results import TerminationCondition, SolutionStatus def get_objective(block): @@ -45,6 +37,47 @@ def get_objective(block): return obj +def check_optimal_termination(results): + """ + This function returns True if the termination condition for the solver + is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + + Parameters + ---------- + results : Pyomo Results object returned from solver.solve + + Returns + ------- + `bool` + """ + if results.solution_status == SolutionStatus.optimal and ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): + return True + return False + + +def assert_optimal_termination(results): + """ + This function checks if the termination condition for the solver + is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + and it raises a RuntimeError exception if this is not true. + + Parameters + ---------- + results : Pyomo Results object returned from solver.solve + """ + if not check_optimal_termination(results): + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solution status: {}, Termination condition: {}'.format( + results.solution_status, results.termination_condition + ) + ) + raise RuntimeError(msg) + + class _VarAndNamedExprCollector(ExpressionValueVisitor): def __init__(self): self.named_expressions = {} From 52b8b6b0670a7742a30a3b1a92d42591b648fa06 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 13:24:03 -0700 Subject: [PATCH 0125/1178] Revert Factory changes; clean up some nonsense --- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/solver/IPOPT.py | 2 +- pyomo/solver/base.py | 1 - pyomo/solver/factory.py | 33 +++++++++++++++++++++++++++++++++ pyomo/solver/plugins.py | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 pyomo/solver/factory.py diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index a8f4390972f..3a132b74395 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.opt.base.solvers import SolverFactory +from pyomo.solver.factory import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 11a6a7c4cb8..30f4cfc60a9 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -22,7 +22,7 @@ from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig -from pyomo.opt.base.solvers import SolverFactory +from pyomo.solver.factory import SolverFactory from pyomo.solver.results import ( Results, TerminationCondition, diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8f0bd8c116f..8c6ef0bddef 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -21,7 +21,6 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError - from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py new file mode 100644 index 00000000000..84b6cf02eac --- /dev/null +++ b/pyomo/solver/factory.py @@ -0,0 +1,33 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +from pyomo.solver.base import LegacySolverInterface + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + # class LegacySolver(LegacySolverInterface, cls): + # pass + + # LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 1dfcb6d2fe5..2f95ca9f410 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from pyomo.opt.base.solvers import SolverFactory +from .factory import SolverFactory from .IPOPT import IPOPT From 63af991b68d2d18bbabd0e22126e59638540c34c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 15:15:29 -0700 Subject: [PATCH 0126/1178] Push changes: pyomo --help solvers tracks all solvers, v1, v2, and appsi --- pyomo/solver/IPOPT.py | 3 ++- pyomo/solver/base.py | 10 ++++++++++ pyomo/solver/factory.py | 7 ++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 30f4cfc60a9..74b0ae25358 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -13,7 +13,7 @@ import subprocess import io import sys -from typing import Mapping +from typing import Mapping, Dict from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt @@ -153,6 +153,7 @@ class IPOPT(SolverBase): def __init__(self, **kwds): self._config = self.CONFIG(kwds) + self.ipopt_options = ipopt_command_line_options def available(self): if self.config.executable.path() is None: diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8c6ef0bddef..d4b46ebe5d4 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -37,6 +37,16 @@ class SolverBase(abc.ABC): + # + # Support "with" statements. Forgetting to call deactivate + # on Plugins is a common source of memory leaks + # + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + pass + class Availability(enum.IntEnum): FullLicense = 2 LimitedLicense = 1 diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py index 84b6cf02eac..23a66acd9cb 100644 --- a/pyomo/solver/factory.py +++ b/pyomo/solver/factory.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory from pyomo.solver.base import LegacySolverInterface @@ -20,10 +21,10 @@ def decorator(cls): self._cls[name] = cls self._doc[name] = doc - # class LegacySolver(LegacySolverInterface, cls): - # pass + class LegacySolver(LegacySolverInterface, cls): + pass - # LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register(name, doc)(LegacySolver) return cls From 5e78aa162fff7f7ca82fcd98364fb89c774cd82a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 16:13:08 -0700 Subject: [PATCH 0127/1178] Certify backwards compatibility --- pyomo/solver/IPOPT.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 74b0ae25358..55c97687b05 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -19,8 +19,9 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.core.base.label import NumericLabeler from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo -from pyomo.solver.base import SolverBase +from pyomo.solver.base import SolverBase, SymbolMap from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory from pyomo.solver.results import ( @@ -153,7 +154,8 @@ class IPOPT(SolverBase): def __init__(self, **kwds): self._config = self.CONFIG(kwds) - self.ipopt_options = ipopt_command_line_options + self._writer = NLWriter() + self.ipopt_options = self._config.solver_options def available(self): if self.config.executable.path() is None: @@ -181,6 +183,10 @@ def config(self): def config(self, val): self._config = val + @property + def symbol_map(self): + return self._symbol_map + def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream for k, val in options.items(): @@ -199,10 +205,10 @@ def _create_command_line(self, basename: str, config: IPOPTConfig): 'Use IPOPT.config.temp_dir to specify the name of the options file. ' 'Do not use IPOPT.config.solver_options["option_file_name"].' ) - ipopt_options = dict(config.solver_options) - if config.time_limit is not None and 'max_cpu_time' not in ipopt_options: - ipopt_options['max_cpu_time'] = config.time_limit - for k, v in ipopt_options.items(): + self.ipopt_options = dict(config.solver_options) + if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: + self.ipopt_options['max_cpu_time'] = config.time_limit + for k, v in self.ipopt_options.items(): cmd.append(str(k) + '=' + str(v)) return cmd @@ -223,8 +229,6 @@ def solve(self, model, **kwds): None, (env.get('AMPLFUNC', None), env.get('PYOMO_AMPLFUNC', None)) ) ) - # Write the model to an nl file - nl_writer = NLWriter() # Need to add check for symbolic_solver_labels; may need to generate up # to three files for nl, row, col, if ssl == True # What we have here may or may not work with IPOPT; will find out when @@ -244,13 +248,19 @@ def solve(self, model, **kwds): with open(basename + '.nl', 'w') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: - self.info = nl_writer.write( + self.info = self._writer.write( model, nl_file, row_file, col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + symbol_map = self._symbol_map = SymbolMap() + labeler = NumericLabeler('component') + for v in self.info.variables: + symbol_map.getSymbol(v, labeler) + for c in self.info.constraints: + symbol_map.getSymbol(c, labeler) with open(basename + '.opt', 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options From 3472d0dff53ae89808adbf387fd1974de2299c90 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 27 Nov 2023 12:48:27 -0700 Subject: [PATCH 0128/1178] Commit other changes --- pyomo/solver/IPOPT.py | 2 +- pyomo/solver/results.py | 2 +- pyomo/solver/util.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 55c97687b05..90a92b0de24 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -13,7 +13,7 @@ import subprocess import io import sys -from typing import Mapping, Dict +from typing import Mapping from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 404977e8a1a..728e47fc7a1 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -397,7 +397,7 @@ def parse_sol_file( while line: remaining += line.strip() + "; " line = sol_file.readline() - result.solver_message += remaining + result.extra_info.solver_message += remaining break unmasked_kind = int(line[1]) kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 16d7c4d7cd4..ec59f7e80f7 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -38,6 +38,8 @@ def get_objective(block): def check_optimal_termination(results): + # TODO: Make work for legacy and new results objects. + # Look at the original version of this function to make that happen. """ This function returns True if the termination condition for the solver is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' From 6114a3c9048863bc372c5d47644c75f5a2ed49b4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 1 Dec 2023 09:32:56 -0700 Subject: [PATCH 0129/1178] Hack around #3045 by just ignoring things that don't have a ctype when I collect components in the docplex writer --- pyomo/contrib/cp/repn/docplex_writer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 51c3f66140e..50a2d72aed8 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1005,6 +1005,9 @@ def collect_valid_components(model, active=True, sort=None, valid=set(), targets unrecognized = {} components = {k: [] for k in targets} for obj in model.component_data_objects(active=True, descend_into=True, sort=sort): + # HACK around #3045 + if not hasattr(obj, 'ctype'): + continue ctype = obj.ctype if ctype in components: components[ctype].append(obj) From 1a3ddbb831fafecc156d2f144a7cf308b8340e1b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 1 Dec 2023 15:11:10 -0700 Subject: [PATCH 0130/1178] Adding SequenceVar component and a couple tests --- pyomo/contrib/cp/sequence_var.py | 133 ++++++++++++++++++++ pyomo/contrib/cp/tests/test_sequence_var.py | 56 +++++++++ 2 files changed, 189 insertions(+) create mode 100644 pyomo/contrib/cp/sequence_var.py create mode 100644 pyomo/contrib/cp/tests/test_sequence_var.py diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py new file mode 100644 index 00000000000..d5553cacd20 --- /dev/null +++ b/pyomo/contrib/cp/sequence_var.py @@ -0,0 +1,133 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import logging + +from pyomo.common.log import is_debug_set +from pyomo.common.modeling import NOTSET +from pyomo.contrib.cp import IntervalVar +from pyomo.core import ModelComponentFactory +from pyomo.core.base.component import ActiveComponentData +from pyomo.core.base.global_set import UnindexedComponent_index +from pyomo.core.base.indexed_component import ActiveIndexedComponent + +import sys +from weakref import ref as weakref_ref + +logger = logging.getLogger(__name__) + + +class _SequenceVarData(ActiveComponentData): + """This class defines the abstract interface for a single sequence variable.""" + __slots__ = ('interval_vars',) + def __init__(self, component=None): + # in-lining ActiveComponentData and ComponentData constructors, as is + # traditional: + self._component = weakref_ref(component) if (component is not None) else None + self._index = NOTSET + self._active = True + + # This thing is really just an ordered set of interval vars that we can + # write constraints over. + self.interval_vars = [] + + def set_value(self, expr): + # We'll demand expr be a list for now--it needs to be ordered so this + # doesn't seem like too much to ask + if expr.__class__ is not list: + raise ValueError( + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '%s' constructing '%s'" % (type(expr), + self.name)) + for v in expr: + if not hasattr(v, 'ctype') or v.ctype is not IntervalVar: + raise ValueError( + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar '%s' included " + "an object of type '%s'" % (self.name, type(v))) + self.interval_vars.append(v) + + +@ModelComponentFactory.register("Sequences of IntervalVars") +class SequenceVar(ActiveIndexedComponent): + _ComponentDataClass = _SequenceVarData + + def __new__(cls, *args, **kwds): + if cls != SequenceVar: + return super(SequenceVar, cls).__new__(cls) + if args == (): + return ScalarSequenceVar.__new__(ScalarSequenceVar) + else: + return IndexedSequenceVar.__new__(IndexedSequenceVar) + + def __init__(self, *args, **kwargs): + self._init_rule = kwargs.pop('rule', None) + self._init_expr = kwargs.pop('expr', None) + kwargs.setdefault('ctype', SequenceVar) + super(SequenceVar, self).__init__(*args, **kwargs) + + if self._init_expr is not None and self._init_rule is not None: + raise ValueError( + "Cannot specify both rule= and expr= for SequenceVar %s" % (self.name,) + ) + + def _getitem_when_not_present(self, index): + if index is None and not self.is_indexed(): + obj = self._data[index] = self + else: + obj = self._data[index] = self._ComponentDataClass(component=self) + parent = self.parent_block() + obj._index = index + + if self._init_rule is not None: + obj.interval_vars = self._init_rule(parent, index) + if self._init_expr is not None: + obj.interval_vars = self._init_expr + + return obj + + def construct(self, data=None): + """ + Construct the _SequenceVarData objects for this SequenceVar + """ + if self._constructed: + return + self._constructed = True + + if is_debug_set(logger): + logger.debug("Constructing SequenceVar %s" % self.name) + + # Initialize index in case we hit the exception below + index = None + try: + if not self.is_indexed(): + self._getitem_when_not_present(None) + if self._init_rule is not None: + for index in self.index_set(): + self._getitem_when_not_present(index) + except Exception: + err = sys.exc_info()[1] + logger.error( + "Rule failed when initializing sequence variable for " + "SequenceVar %s with index %s:\n%s: %s" + % (self.name, str(index), type(err).__name__, err) + ) + raise + +class ScalarSequenceVar(_SequenceVarData, SequenceVar): + def __init__(self, *args, **kwds): + _SequenceVarData.__init__(self, component=self) + SequenceVar.__init__(self, *args, **kwds) + self._index = UnindexedComponent_index + + +class IndexedSequenceVar(SequenceVar): + pass diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py new file mode 100644 index 00000000000..9a2278d2de3 --- /dev/null +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -0,0 +1,56 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar +from pyomo.environ import ConcreteModel, Integers, Set, value, Var + + +class TestScalarSequenceVar(unittest.TestCase): + def test_initialize_with_no_data(self): + m = ConcreteModel() + m.i = SequenceVar() + + self.assertIsInstance(m.i, SequenceVar) + self.assertIsInstance(m.i.interval_vars, list) + self.assertEqual(len(m.i.interval_vars), 0) + + def test_initialize_with_expr(self): + m = ConcreteModel() + m.S = Set(initialize=range(3)) + m.i = IntervalVar(m.S, start=(0, 5)) + m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + self.assertEqual(len(m.seq.interval_vars), 3) + for j in m.S: + self.assertIs(m.seq.interval_vars[j], m.i[j]) + + +class TestIndexedSequenceVar(unittest.TestCase): + def test_initialize_with_rule(self): + m = ConcreteModel() + m.alph = Set(initialize=['a', 'b']) + m.num = Set(initialize=[1, 2]) + m.i = IntervalVar(m.alph, m.num) + + def the_rule(m, j): + return [m.i[j, k] for k in m.num] + m.seq = SequenceVar(m.alph, rule=the_rule) + m.seq.pprint() + + self.assertIsInstance(m.seq, IndexedSequenceVar) + self.assertEqual(len(m.seq), 2) + for j in m.alph: + self.assertTrue(j in m.seq) + self.assertEqual(len(m.seq[j].interval_vars), 2) + for k in m.num: + self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) + From 9dc5aa6bab909504c6042e2a0c2bd793506896c4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 1 Dec 2023 15:30:49 -0700 Subject: [PATCH 0131/1178] Adding a pretty pprint --- pyomo/contrib/cp/sequence_var.py | 15 ++++++ pyomo/contrib/cp/tests/test_sequence_var.py | 51 +++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index d5553cacd20..412c34e9176 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -122,6 +122,21 @@ def construct(self, data=None): ) raise + def _pprint(self): + """Print component information.""" + headers = [ + ("Size", len(self)), + ("Index", self._index_set if self.is_indexed() else None), + ] + return ( + headers, + self._data.items(), + ("IntervalVars",), + lambda k, v: [ + '[' + ', '.join(iv.name for iv in v.interval_vars) + ']', + ] + ) + class ScalarSequenceVar(_SequenceVarData, SequenceVar): def __init__(self, *args, **kwds): _SequenceVarData.__init__(self, component=self) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 9a2278d2de3..da9b5a298d3 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from io import StringIO import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar @@ -24,18 +25,44 @@ def test_initialize_with_no_data(self): self.assertIsInstance(m.i.interval_vars, list) self.assertEqual(len(m.i.interval_vars), 0) - def test_initialize_with_expr(self): + def get_model(self): m = ConcreteModel() m.S = Set(initialize=range(3)) m.i = IntervalVar(m.S, start=(0, 5)) m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + + return m + + def test_initialize_with_expr(self): + m = self.get_model() self.assertEqual(len(m.seq.interval_vars), 3) for j in m.S: self.assertIs(m.seq.interval_vars[j], m.i[j]) + def test_pprint(self): + m = self.get_model() + buf = StringIO() + m.seq.pprint(ostream=buf) + self.assertEqual( + buf.getvalue().strip(), + """ +seq : Size=1, Index=None + Key : IntervalVars + None : [i[0], i[1], i[2]] + """.strip() + ) class TestIndexedSequenceVar(unittest.TestCase): - def test_initialize_with_rule(self): + def test_initialize_with_not_data(self): + m = ConcreteModel() + m.i = SequenceVar([1, 2]) + + self.assertIsInstance(m.i, IndexedSequenceVar) + for j in [1, 2]: + self.assertIsInstance(m.i[j].interval_vars, list) + self.assertEqual(len(m.i[j].interval_vars), 0) + + def make_model(self): m = ConcreteModel() m.alph = Set(initialize=['a', 'b']) m.num = Set(initialize=[1, 2]) @@ -44,7 +71,11 @@ def test_initialize_with_rule(self): def the_rule(m, j): return [m.i[j, k] for k in m.num] m.seq = SequenceVar(m.alph, rule=the_rule) - m.seq.pprint() + + return m + + def test_initialize_with_rule(self): + m = self.make_model() self.assertIsInstance(m.seq, IndexedSequenceVar) self.assertEqual(len(m.seq), 2) @@ -54,3 +85,17 @@ def the_rule(m, j): for k in m.num: self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) + def test_pprint(self): + m = self.make_model() + m.seq.pprint() + + buf = StringIO() + m.seq.pprint(ostream=buf) + self.assertEqual( + buf.getvalue().strip(), + """ +seq : Size=2, Index=alph + Key : IntervalVars + a : [i[a,1], i[a,2]] + b : [i[b,1], i[b,2]]""".strip() + ) From 2e6ce49469f3cc868d940aa1b65fb629d69cc719 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 1 Dec 2023 15:41:33 -0700 Subject: [PATCH 0132/1178] pyomo.solver.ipopt: account for presolve when loading results --- pyomo/contrib/appsi/solvers/wntr.py | 15 ++++++--------- pyomo/solver/IPOPT.py | 27 ++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 0a358c6aedf..3d1d36586e0 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,11 +1,8 @@ -from pyomo.contrib.appsi.base import ( - PersistentBase, - PersistentSolver, - SolverConfig, - Results, - TerminationCondition, - PersistentSolutionLoader, -) +from pyomo.solver.base import PersistentSolverBase +from pyomo.solver.util import PersistentSolverUtils +from pyomo.solver.config import SolverConfig, ConfigValue +from pyomo.solver.results import Results, TerminationCondition +from pyomo.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( ProductExpression, DivisionExpression, @@ -73,7 +70,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class Wntr(PersistentBase, PersistentSolver): +class Wntr(PersistentSolverUtils, PersistentSolverBase): def __init__(self, only_child_vars=True): super().__init__(only_child_vars=only_child_vars) self._config = WntrConfig() diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 90a92b0de24..c22b0e39857 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -20,7 +20,7 @@ from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.core.base.label import NumericLabeler -from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn from pyomo.solver.base import SolverBase, SymbolMap from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory @@ -155,6 +155,8 @@ class IPOPT(SolverBase): def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() + self._writer.config.skip_trivial_constraints = True + self._writer.config.linear_presolve = True self.ipopt_options = self._config.solver_options def available(self): @@ -279,10 +281,10 @@ def solve(self, model, **kwds): ostreams = [ LogStream( - level=self.config.log_level, logger=self.config.solver_output_logger + level=config.log_level, logger=config.solver_output_logger ) ] - if self.config.tee: + if config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: process = subprocess.run( @@ -376,6 +378,14 @@ def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) + if len(nl_info.eliminated_vars) > 0: + sub_map = {k: v[1] for k, v in sol_data.primals.items()} + for v, v_expr in nl_info.eliminated_vars: + val = evaluate_ampl_repn(v_expr, sub_map) + v_id = id(v) + sub_map[v_id] = val + sol_data.primals[v_id] = (v, val) + res.solution_loader = SolutionLoader( primals=sol_data.primals, duals=sol_data.duals, @@ -384,3 +394,14 @@ def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): ) return res + + +def evaluate_ampl_repn(repn: AMPLRepn, sub_map): + assert not repn.nonlinear + assert repn.nl is None + val = repn.const + if repn.linear is not None: + for v_id, v_coef in repn.linear.items(): + val += v_coef * sub_map[v_id] + val *= repn.mult + return val \ No newline at end of file From 749b4e81dd725a919cc945af039d02baad145af0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 4 Dec 2023 07:21:08 -0700 Subject: [PATCH 0133/1178] Adding some sequence var tests, making sure we hit set_value when we want error checking --- pyomo/contrib/cp/sequence_var.py | 6 ++-- pyomo/contrib/cp/tests/test_sequence_var.py | 40 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 412c34e9176..b0691fbd74a 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -42,7 +42,7 @@ def __init__(self, component=None): def set_value(self, expr): # We'll demand expr be a list for now--it needs to be ordered so this # doesn't seem like too much to ask - if expr.__class__ is not list: + if not hasattr(expr, '__iter__'): raise ValueError( "'expr' for SequenceVar must be a list of IntervalVars. " "Encountered type '%s' constructing '%s'" % (type(expr), @@ -88,9 +88,9 @@ def _getitem_when_not_present(self, index): obj._index = index if self._init_rule is not None: - obj.interval_vars = self._init_rule(parent, index) + obj.set_value(self._init_rule(parent, index)) if self._init_expr is not None: - obj.interval_vars = self._init_expr + obj.set_value(self._init_expr) return obj diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index da9b5a298d3..852d9f2134a 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -25,6 +25,15 @@ def test_initialize_with_no_data(self): self.assertIsInstance(m.i.interval_vars, list) self.assertEqual(len(m.i.interval_vars), 0) + m.iv1 = IntervalVar() + m.iv2 = IntervalVar() + m.i.set_value(expr=[m.iv1, m.iv2]) + + self.assertIsInstance(m.i.interval_vars, list) + self.assertEqual(len(m.i.interval_vars), 2) + self.assertIs(m.i.interval_vars[0], m.iv1) + self.assertIs(m.i.interval_vars[1], m.iv2) + def get_model(self): m = ConcreteModel() m.S = Set(initialize=range(3)) @@ -52,6 +61,27 @@ def test_pprint(self): """.strip() ) + def test_interval_vars_not_a_list(self): + m = self.get_model() + + with self.assertRaisesRegex( + ValueError, + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '' constructing 'seq2'" + ): + m.seq2 = SequenceVar(expr=1) + + def test_interval_vars_list_includes_things_that_are_not_interval_vars(self): + m = self.get_model() + + with self.assertRaisesRegex( + ValueError, + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar 'seq2' included " + "an object of type ''" + ): + m.seq2 = SequenceVar(expr=m.i) + class TestIndexedSequenceVar(unittest.TestCase): def test_initialize_with_not_data(self): m = ConcreteModel() @@ -62,6 +92,16 @@ def test_initialize_with_not_data(self): self.assertIsInstance(m.i[j].interval_vars, list) self.assertEqual(len(m.i[j].interval_vars), 0) + m.iv = IntervalVar() + m.iv2 = IntervalVar([0, 1]) + m.i[2] = [m.iv] + [m.iv2[i] for i in [0, 1]] + + self.assertEqual(len(m.i[2].interval_vars), 3) + self.assertEqual(len(m.i[1].interval_vars), 0) + self.assertIs(m.i[2].interval_vars[0], m.iv) + for i in [0, 1]: + self.assertIs(m.i[2].interval_vars[i + 1], m.iv2[i]) + def make_model(self): m = ConcreteModel() m.alph = Set(initialize=['a', 'b']) From 3f3b3c364fb20c740f83982081d4deccb50ffc2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 09:44:11 -0700 Subject: [PATCH 0134/1178] Add timing capture and iteration/log parsing --- pyomo/solver/IPOPT.py | 109 +++++++++++++++++++++++++++------------- pyomo/solver/plugins.py | 4 +- pyomo/solver/results.py | 6 ++- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 90a92b0de24..5e145a5743f 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -11,6 +11,7 @@ import os import subprocess +import datetime import io import sys from typing import Mapping @@ -50,7 +51,7 @@ class SolverError(PyomoException): pass -class IPOPTConfig(SolverConfig): +class ipoptConfig(SolverConfig): def __init__( self, description=None, @@ -84,7 +85,7 @@ def __init__( ) -class IPOPTSolutionLoader(SolutionLoaderBase): +class ipoptSolutionLoader(SolutionLoaderBase): pass @@ -148,9 +149,9 @@ class IPOPTSolutionLoader(SolutionLoaderBase): } -@SolverFactory.register('ipopt_v2', doc='The IPOPT NLP solver (new interface)') -class IPOPT(SolverBase): - CONFIG = IPOPTConfig() +@SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)') +class ipopt(SolverBase): + CONFIG = ipoptConfig() def __init__(self, **kwds): self._config = self.CONFIG(kwds) @@ -193,7 +194,7 @@ def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): if k not in ipopt_command_line_options: f.write(str(k) + ' ' + str(val) + '\n') - def _create_command_line(self, basename: str, config: IPOPTConfig): + def _create_command_line(self, basename: str, config: ipoptConfig): cmd = [ str(config.executable), basename + '.nl', @@ -202,8 +203,8 @@ def _create_command_line(self, basename: str, config: IPOPTConfig): ] if 'option_file_name' in config.solver_options: raise ValueError( - 'Use IPOPT.config.temp_dir to specify the name of the options file. ' - 'Do not use IPOPT.config.solver_options["option_file_name"].' + 'Use ipopt.config.temp_dir to specify the name of the options file. ' + 'Do not use ipopt.config.solver_options["option_file_name"].' ) self.ipopt_options = dict(config.solver_options) if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: @@ -214,25 +215,15 @@ def _create_command_line(self, basename: str, config: IPOPTConfig): return cmd def solve(self, model, **kwds): + # Begin time tracking + start_timestamp = datetime.datetime.now(datetime.timezone.utc) # Check if solver is available avail = self.available() if not avail: raise SolverError(f'Solver {self.__class__} is not available ({avail}).') # Update configuration options, based on keywords passed to solve - config: IPOPTConfig = self.config(kwds.pop('options', {})) + config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) - # Get a copy of the environment to pass to the subprocess - env = os.environ.copy() - if 'PYOMO_AMPLFUNC' in env: - env['AMPLFUNC'] = "\n".join( - filter( - None, (env.get('AMPLFUNC', None), env.get('PYOMO_AMPLFUNC', None)) - ) - ) - # Need to add check for symbolic_solver_labels; may need to generate up - # to three files for nl, row, col, if ssl == True - # What we have here may or may not work with IPOPT; will find out when - # we try to run it. with TempfileManager.new_context() as tempfile: if config.temp_dir is None: dname = tempfile.mkdtemp() @@ -248,24 +239,30 @@ def solve(self, model, **kwds): with open(basename + '.nl', 'w') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: - self.info = self._writer.write( + nl_info = self._writer.write( model, nl_file, row_file, col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if nl_info.external_function_libraries: + if env.get('AMPLFUNC'): + nl_info.external_function_libraries.append(env.get('AMPLFUNC')) + env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) symbol_map = self._symbol_map = SymbolMap() labeler = NumericLabeler('component') - for v in self.info.variables: + for v in nl_info.variables: symbol_map.getSymbol(v, labeler) - for c in self.info.constraints: + for c in nl_info.constraints: symbol_map.getSymbol(c, labeler) with open(basename + '.opt', 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options ) - # Call IPOPT - passing the files via the subprocess + # Call ipopt - passing the files via the subprocess cmd = self._create_command_line(basename=basename, config=config) # this seems silly, but we have to give the subprocess slightly longer to finish than @@ -277,13 +274,15 @@ def solve(self, model, **kwds): else: timeout = None - ostreams = [ - LogStream( - level=self.config.log_level, logger=self.config.solver_output_logger - ) - ] - if self.config.tee: + ostreams = [io.StringIO()] + if config.tee: ostreams.append(sys.stdout) + else: + ostreams.append( + LogStream( + level=config.log_level, logger=config.solver_output_logger + ) + ) with TeeStream(*ostreams) as t: process = subprocess.run( cmd, @@ -293,16 +292,19 @@ def solve(self, model, **kwds): stdout=t.STDOUT, stderr=t.STDERR, ) + # This is the stuff we need to parse to get the iterations + # and time + iters, solver_time = self._parse_ipopt_output(ostreams[0]) if process.returncode != 0: results = Results() results.termination_condition = TerminationCondition.error results.solution_loader = SolutionLoader(None, None, None, None) else: - # TODO: Make a context manager out of this and open the file - # to pass to the results, instead of doing this thing. with open(basename + '.sol', 'r') as sol_file: - results = self._parse_solution(sol_file, self.info) + results = self._parse_solution(sol_file, nl_info) + results.iteration_count = iters + results.timing_info.solver_wall_time = solver_time if ( config.raise_exception_on_nonoptimal_result @@ -340,10 +342,10 @@ def solve(self, model, **kwds): if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: if config.load_solution: - results.incumbent_objective = value(self.info.objectives[0]) + results.incumbent_objective = value(nl_info.objectives[0]) else: results.incumbent_objective = replace_expressions( - self.info.objectives[0].expr, + nl_info.objectives[0].expr, substitution_map={ id(v): val for v, val in results.solution_loader.get_primals().items() @@ -352,8 +354,43 @@ def solve(self, model, **kwds): remove_named_expressions=True, ) + # Capture/record end-time / wall-time + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + results.timing_info.start_timestamp = start_timestamp + results.timing_info.wall_time = ( + end_timestamp - start_timestamp + ).total_seconds() return results + def _parse_ipopt_output(self, stream: io.StringIO): + """ + Parse an IPOPT output file and return: + + * number of iterations + * time in IPOPT + + """ + + iters = None + time = None + # parse the output stream to get the iteration count and solver time + for line in stream.getvalue().splitlines(): + if line.startswith("Number of Iterations....:"): + tokens = line.split() + iters = int(tokens[3]) + elif line.startswith( + "Total CPU secs in IPOPT (w/o function evaluations) =" + ): + tokens = line.split() + time = float(tokens[9]) + elif line.startswith( + "Total CPU secs in NLP function evaluations =" + ): + tokens = line.split() + time += float(tokens[8]) + + return iters, time + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] res, sol_data = parse_sol_file( diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 2f95ca9f410..54d03eaf74b 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -11,10 +11,10 @@ from .factory import SolverFactory -from .IPOPT import IPOPT +from .ipopt import ipopt def load(): SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( - IPOPT + ipopt ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 728e47fc7a1..71e92a1539f 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -228,9 +228,11 @@ def __init__( ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) - self.timing_info.start_time: datetime = self.timing_info.declare( - 'start_time', ConfigValue(domain=Datetime) + self.timing_info.start_timestamp: datetime = self.timing_info.declare( + 'start_timestamp', ConfigValue(domain=Datetime) ) + # wall_time is the actual standard (until Michael complains) that is + # required for everyone. This is from entry->exit of the solve method. self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) ) From 5138ae8c6f7fdcef1ac954db2b617e2417b9cd26 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 09:55:37 -0700 Subject: [PATCH 0135/1178] Change to lowercase ipopt --- pyomo/solver/{IPOPT.py => ipopt.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pyomo/solver/{IPOPT.py => ipopt.py} (100%) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/ipopt.py similarity index 100% rename from pyomo/solver/IPOPT.py rename to pyomo/solver/ipopt.py From 5a9e5e598660f99a85adac16e29d7b69ce2c012c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 09:58:16 -0700 Subject: [PATCH 0136/1178] Blackify --- pyomo/solver/ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 6196d230ec3..8f987c0e3c7 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -441,4 +441,4 @@ def evaluate_ampl_repn(repn: AMPLRepn, sub_map): for v_id, v_coef in repn.linear.items(): val += v_coef * sub_map[v_id] val *= repn.mult - return val \ No newline at end of file + return val From a503d45b7989a1c8363a518bbdeeed65a9bb946d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:04:02 -0700 Subject: [PATCH 0137/1178] Test file needed updated --- pyomo/solver/tests/solvers/test_ipopt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index 8cf046fcfef..abf7287489f 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -13,12 +13,12 @@ import pyomo.environ as pyo from pyomo.common.fileutils import ExecutableData from pyomo.common.config import ConfigDict -from pyomo.solver.IPOPT import IPOPTConfig +from pyomo.solver.ipopt import ipoptConfig from pyomo.solver.factory import SolverFactory from pyomo.common import unittest -class TestIPOPT(unittest.TestCase): +class TestIpopt(unittest.TestCase): def create_model(self): model = pyo.ConcreteModel() model.x = pyo.Var(initialize=1.5) @@ -30,9 +30,9 @@ def rosenbrock(m): model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) return model - def test_IPOPT_config(self): + def test_ipopt_config(self): # Test default initialization - config = IPOPTConfig() + config = ipoptConfig() self.assertTrue(config.load_solution) self.assertIsInstance(config.solver_options, ConfigDict) print(type(config.executable)) From d04e1f8d7d5f39143022f62688ff06cbba1f5e30 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:10:20 -0700 Subject: [PATCH 0138/1178] Anotther test was incorrect --- pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index d250923f104..1644eab4008 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.results import TerminationCondition from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math From 6588232f7f93345da5e159a2eddc05d95db347b2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:19:01 -0700 Subject: [PATCH 0139/1178] Remove cmodel extensions --- pyomo/contrib/appsi/solvers/wntr.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 3d1d36586e0..5b7f2de8592 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,6 +1,6 @@ from pyomo.solver.base import PersistentSolverBase from pyomo.solver.util import PersistentSolverUtils -from pyomo.solver.config import SolverConfig, ConfigValue +from pyomo.solver.config import SolverConfig from pyomo.solver.results import Results, TerminationCondition from pyomo.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( @@ -33,7 +33,6 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available wntr, wntr_available = attempt_import('wntr') import logging @@ -209,8 +208,6 @@ def set_instance(self, model): ) self._reinit() self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() From 77f65969bf0b16baff7989927fa67f9223b95c0f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:30:57 -0700 Subject: [PATCH 0140/1178] More conversion in Wntr needed --- pyomo/contrib/appsi/solvers/wntr.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 5b7f2de8592..649b9aa2479 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,7 +1,7 @@ from pyomo.solver.base import PersistentSolverBase from pyomo.solver.util import PersistentSolverUtils from pyomo.solver.config import SolverConfig -from pyomo.solver.results import Results, TerminationCondition +from pyomo.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( ProductExpression, @@ -122,7 +122,7 @@ def _solve(self, timer: HierarchicalTimer): options.update(self.wntr_options) opt = wntr.sim.solvers.NewtonSolver(options) - if self.config.stream_solver: + if self.config.tee: ostream = sys.stdout else: ostream = None @@ -139,13 +139,12 @@ def _solve(self, timer: HierarchicalTimer): tf = time.time() results = WntrResults(self) - results.wallclock_time = tf - t0 + results.timing_info.wall_time = tf - t0 if status == wntr.sim.solvers.SolverStatus.converged: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal else: results.termination_condition = TerminationCondition.error - results.best_feasible_objective = None - results.best_objective_bound = None if self.config.load_solution: if status == wntr.sim.solvers.SolverStatus.converged: @@ -157,7 +156,7 @@ def _solve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) return results From 7f76ff4bf0b1ae62d1166b003e8a1cff9cd13aa6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:33:56 -0700 Subject: [PATCH 0141/1178] Update Results test --- pyomo/solver/tests/test_results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 5392c1135f8..bf822594002 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -99,7 +99,7 @@ def test_uninitialized(self): self.assertIsNone(res.iteration_count) self.assertIsInstance(res.timing_info, ConfigDict) self.assertIsInstance(res.extra_info, ConfigDict) - self.assertIsNone(res.timing_info.start_time) + self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) self.assertIsNone(res.timing_info.solver_wall_time) res.solution_loader = solution.SolutionLoader(None, None, None, None) From 09e3fe946f49b92263116413de298817cb88a431 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:36:56 -0700 Subject: [PATCH 0142/1178] Blackify --- pyomo/contrib/appsi/solvers/wntr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 649b9aa2479..70af135e681 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -141,7 +141,9 @@ def _solve(self, timer: HierarchicalTimer): results = WntrResults(self) results.timing_info.wall_time = tf - t0 if status == wntr.sim.solvers.SolverStatus.converged: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) results.solution_status = SolutionStatus.optimal else: results.termination_condition = TerminationCondition.error From f26625eaf8cd9b345132d9d2bb106accb820cdce Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:43:52 -0700 Subject: [PATCH 0143/1178] wallclock attribute no longer valid --- pyomo/contrib/appsi/solvers/wntr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 70af135e681..aaa130f8631 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -65,7 +65,6 @@ def __init__( class WntrResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) From a2efd8d8931e85644d50ca07163b807c096cde31 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 11:08:57 -0700 Subject: [PATCH 0144/1178] Update TerminationCondition and SolutionStatus checks --- .../solvers/tests/test_wntr_persistent.py | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index 1644eab4008..50058262488 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.solver.results import TerminationCondition +from pyomo.solver.results import TerminationCondition, SolutionStatus from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math @@ -18,12 +18,14 @@ def test_param_updates(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) m.p.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) def test_remove_add_constraint(self): @@ -36,7 +38,8 @@ def test_remove_add_constraint(self): opt.config.symbolic_solver_labels = True opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -45,7 +48,8 @@ def test_remove_add_constraint(self): m.x.value = 0.5 m.y.value = 0.5 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 0) @@ -58,21 +62,24 @@ def test_fixed_var(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) m.x.unfix() m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) m.x.fix(0.5) del m.c2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -89,7 +96,8 @@ def test_remove_variables_params(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) self.assertAlmostEqual(m.z.value, 0) @@ -100,14 +108,16 @@ def test_remove_variables_params(self): m.z.value = 2 m.px.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) self.assertAlmostEqual(m.z.value, 2) del m.z m.px.value = 3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 3) def test_get_primals(self): @@ -120,7 +130,8 @@ def test_get_primals(self): opt.config.load_solution = False opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) primals = opt.get_primals() @@ -134,49 +145,57 @@ def test_operators(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) del m.c1 m.x.value = 0 m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 2) del m.c1 m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) m.x.value = 0 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) del m.c1 m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) From 682c8b8aff32599d09d47478fba76c972e8ea706 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 11:10:54 -0700 Subject: [PATCH 0145/1178] Blackify... again --- .../solvers/tests/test_wntr_persistent.py | 76 ++++++++++++++----- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index 50058262488..971305001a9 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -18,13 +18,17 @@ def test_param_updates(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) m.p.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) @@ -38,7 +42,9 @@ def test_remove_add_constraint(self): opt.config.symbolic_solver_labels = True opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -48,7 +54,9 @@ def test_remove_add_constraint(self): m.x.value = 0.5 m.y.value = 0.5 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 0) @@ -62,7 +70,9 @@ def test_fixed_var(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -70,7 +80,9 @@ def test_fixed_var(self): m.x.unfix() m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -78,7 +90,9 @@ def test_fixed_var(self): m.x.fix(0.5) del m.c2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -96,7 +110,9 @@ def test_remove_variables_params(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) @@ -108,7 +124,9 @@ def test_remove_variables_params(self): m.z.value = 2 m.px.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) self.assertAlmostEqual(m.z.value, 2) @@ -116,7 +134,9 @@ def test_remove_variables_params(self): del m.z m.px.value = 3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 3) @@ -130,7 +150,9 @@ def test_get_primals(self): opt.config.load_solution = False opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) @@ -145,7 +167,9 @@ def test_operators(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) @@ -153,14 +177,18 @@ def test_operators(self): m.x.value = 0 m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 2) @@ -168,34 +196,44 @@ def test_operators(self): m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) m.x.value = 0 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) del m.c1 m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) From 39eb268829ebb2184cc8d62d60d16c3acdc9079e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 5 Dec 2023 14:43:43 -0700 Subject: [PATCH 0146/1178] Apply options files updates --- pyomo/common/tests/test_config.py | 2 +- pyomo/solver/ipopt.py | 126 +++++++++++++++++++++--------- pyomo/solver/results.py | 12 +-- 3 files changed, 92 insertions(+), 48 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 9bafd852eb9..bf6786ba2a0 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -1473,7 +1473,7 @@ def test_parseDisplay_userdata_add_block_nonDefault(self): self.config.add("bar", ConfigDict(implicit=True)).add("baz", ConfigDict()) test = _display(self.config, 'userdata') sys.stdout.write(test) - self.assertEqual(yaml_load(test), {'bar': {'baz': None}, foo: 0}) + self.assertEqual(yaml_load(test), {'bar': {'baz': None}, 'foo': 0}) @unittest.skipIf(not yaml_available, "Test requires PyYAML") def test_parseDisplay_userdata_add_block(self): diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 8f987c0e3c7..fbe7c0f5604 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -14,10 +14,10 @@ import datetime import io import sys -from typing import Mapping +from typing import Mapping, Optional from pyomo.common import Executable -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.core.base.label import NumericLabeler @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) -class SolverError(PyomoException): +class ipoptSolverError(PyomoException): """ General exception to catch solver system errors """ @@ -74,6 +74,7 @@ def __init__( self.save_solver_io: bool = self.declare( 'save_solver_io', ConfigValue(domain=bool, default=False) ) + # TODO: Add in a deprecation here for keepfiles self.temp_dir: str = self.declare( 'temp_dir', ConfigValue(domain=str, default=None) ) @@ -85,6 +86,34 @@ def __init__( ) +class ipoptResults(Results): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.timing_info.no_function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) + self.timing_info.function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) + + class ipoptSolutionLoader(SolutionLoaderBase): pass @@ -190,30 +219,37 @@ def config(self, val): def symbol_map(self): return self._symbol_map - def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): - f = ostream - for k, val in options.items(): - if k not in ipopt_command_line_options: - f.write(str(k) + ' ' + str(val) + '\n') - - def _create_command_line(self, basename: str, config: ipoptConfig): - cmd = [ - str(config.executable), - basename + '.nl', - '-AMPL', - 'option_file_name=' + basename + '.opt', - ] + def _write_options_file(self, filename: str, options: Mapping): + # First we need to determine if we even need to create a file. + # If options is empty, then we return False + opt_file_exists = False + if not options: + return False + # If it has options in it, parse them and write them to a file. + # If they are command line options, ignore them; they will be + # parsed during _create_command_line + with open(filename + '.opt', 'w') as opt_file: + for k, val in options.items(): + if k not in ipopt_command_line_options: + opt_file_exists = True + opt_file.write(str(k) + ' ' + str(val) + '\n') + return opt_file_exists + + def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: bool): + cmd = [str(config.executable), basename + '.nl', '-AMPL'] + if opt_file: + cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: raise ValueError( - 'Use ipopt.config.temp_dir to specify the name of the options file. ' - 'Do not use ipopt.config.solver_options["option_file_name"].' + 'Pyomo generates the ipopt options file as part of the solve method. ' + 'Add all options to ipopt.config.solver_options instead.' ) self.ipopt_options = dict(config.solver_options) if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: self.ipopt_options['max_cpu_time'] = config.time_limit - for k, v in self.ipopt_options.items(): - cmd.append(str(k) + '=' + str(v)) - + for k, val in self.ipopt_options.items(): + if k in ipopt_command_line_options: + cmd.append(str(k) + '=' + str(val)) return cmd def solve(self, model, **kwds): @@ -222,10 +258,13 @@ def solve(self, model, **kwds): # Check if solver is available avail = self.available() if not avail: - raise SolverError(f'Solver {self.__class__} is not available ({avail}).') + raise ipoptSolverError( + f'Solver {self.__class__} is not available ({avail}).' + ) # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) + results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.temp_dir is None: dname = tempfile.mkdtemp() @@ -260,13 +299,15 @@ def solve(self, model, **kwds): symbol_map.getSymbol(v, labeler) for c in nl_info.constraints: symbol_map.getSymbol(c, labeler) - with open(basename + '.opt', 'w') as opt_file: - self._write_options_file( - ostream=opt_file, options=config.solver_options - ) + # Write the opt_file, if there should be one; return a bool to say + # whether or not we have one (so we can correctly build the command line) + opt_file = self._write_options_file( + filename=basename, options=config.solver_options + ) # Call ipopt - passing the files via the subprocess - cmd = self._create_command_line(basename=basename, config=config) - + cmd = self._create_command_line( + basename=basename, config=config, opt_file=opt_file + ) # this seems silly, but we have to give the subprocess slightly longer to finish than # ipopt if config.time_limit is not None: @@ -296,18 +337,19 @@ def solve(self, model, **kwds): ) # This is the stuff we need to parse to get the iterations # and time - iters, solver_time = self._parse_ipopt_output(ostreams[0]) + iters, ipopt_time_nofunc, ipopt_time_func = self._parse_ipopt_output( + ostreams[0] + ) if process.returncode != 0: - results = Results() results.termination_condition = TerminationCondition.error results.solution_loader = SolutionLoader(None, None, None, None) else: with open(basename + '.sol', 'r') as sol_file: - results = self._parse_solution(sol_file, nl_info) + results = self._parse_solution(sol_file, nl_info, results) results.iteration_count = iters - results.timing_info.solver_wall_time = solver_time - + results.timing_info.no_function_solve_time = ipopt_time_nofunc + results.timing_info.function_solve_time = ipopt_time_func if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -374,7 +416,8 @@ def _parse_ipopt_output(self, stream: io.StringIO): """ iters = None - time = None + nofunc_time = None + func_time = None # parse the output stream to get the iteration count and solver time for line in stream.getvalue().splitlines(): if line.startswith("Number of Iterations....:"): @@ -384,19 +427,24 @@ def _parse_ipopt_output(self, stream: io.StringIO): "Total CPU secs in IPOPT (w/o function evaluations) =" ): tokens = line.split() - time = float(tokens[9]) + nofunc_time = float(tokens[9]) elif line.startswith( "Total CPU secs in NLP function evaluations =" ): tokens = line.split() - time += float(tokens[8]) + func_time = float(tokens[8]) - return iters, time + return iters, nofunc_time, func_time - def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + def _parse_solution( + self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults + ): suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] res, sol_data = parse_sol_file( - sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read + sol_file=instream, + nl_info=nl_info, + suffixes_to_read=suffixes_to_read, + result=result, ) if res.solution_status == SolutionStatus.noSolution: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 71e92a1539f..0aa78bef6bc 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -231,14 +231,9 @@ def __init__( self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue(domain=Datetime) ) - # wall_time is the actual standard (until Michael complains) that is - # required for everyone. This is from entry->exit of the solve method. self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) ) - self.timing_info.solver_wall_time: Optional[float] = self.timing_info.declare( - 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) - ) self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) @@ -267,7 +262,10 @@ def __init__(self) -> None: def parse_sol_file( - sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str] + sol_file: io.TextIOBase, + nl_info: NLWriterInfo, + suffixes_to_read: Sequence[str], + result: Results, ) -> Tuple[Results, SolFileData]: suffixes_to_read = set(suffixes_to_read) sol_data = SolFileData() @@ -275,8 +273,6 @@ def parse_sol_file( # # Some solvers (minto) do not write a message. We will assume # all non-blank lines up the 'Options' line is the message. - result = Results() - # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will # consider to be the solver message. From 396ebce1e962fe2423e89b6c512aa6fe859317bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 5 Dec 2023 14:54:06 -0700 Subject: [PATCH 0147/1178] Fix tests; add TODO notes --- pyomo/solver/ipopt.py | 1 + pyomo/solver/tests/solvers/test_ipopt.py | 9 +++++++++ pyomo/solver/tests/test_results.py | 1 - 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index fbe7c0f5604..092b279269b 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -186,6 +186,7 @@ def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() self._writer.config.skip_trivial_constraints = True + # TODO: Make this an option; not always turned on self._writer.config.linear_presolve = True self.ipopt_options = self._config.solver_options diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index abf7287489f..e157321b4cc 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -18,6 +18,15 @@ from pyomo.common import unittest +""" +TODO: + - Test unique configuration options + - Test unique results options + - Ensure that `*.opt` file is only created when needed + - Ensure options are correctly parsing to env or opt file + - Failures at appropriate times +""" + class TestIpopt(unittest.TestCase): def create_model(self): model = pyo.ConcreteModel() diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index bf822594002..0c0b4bb18db 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -101,7 +101,6 @@ def test_uninitialized(self): self.assertIsInstance(res.extra_info, ConfigDict) self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) - self.assertIsNone(res.timing_info.solver_wall_time) res.solution_loader = solution.SolutionLoader(None, None, None, None) with self.assertRaisesRegex( From e19d440800260b973847fdc51f5c88ea7ac1dfb3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 5 Dec 2023 14:56:08 -0700 Subject: [PATCH 0148/1178] Blackify - adding a single empty space --- pyomo/solver/tests/solvers/test_ipopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index e157321b4cc..d9fccbb84fc 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -27,6 +27,7 @@ - Failures at appropriate times """ + class TestIpopt(unittest.TestCase): def create_model(self): model = pyo.ConcreteModel() From 23c1ce3e2e869ab7bc6388c6c35cecb53c92492a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 10 Dec 2023 13:00:29 -0700 Subject: [PATCH 0149/1178] Adding some expression nodes involving sequence vars and some tests for them --- pyomo/contrib/cp/__init__.py | 8 + .../scheduling_expr/sequence_expressions.py | 173 ++++++++++++++++++ .../cp/tests/test_sequence_expressions.py | 104 +++++++++++ 3 files changed, 285 insertions(+) create mode 100644 pyomo/contrib/cp/scheduling_expr/sequence_expressions.py create mode 100644 pyomo/contrib/cp/tests/test_sequence_expressions.py diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index c51160bf931..03196537446 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -6,6 +6,14 @@ IntervalVarPresence, ) from pyomo.contrib.cp.repn.docplex_writer import DocplexWriter, CPOptimizerSolver +from pyomo.contrib.cp.sequence_var import SequenceVar +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + no_overlap, + first_in_sequence, + last_in_sequence, + before_in_sequence, + predecessor_to, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py new file mode 100644 index 00000000000..0a49198c1d1 --- /dev/null +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -0,0 +1,173 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core.expr.logical_expr import BooleanExpression + +# ESJ TODO: The naming in this file needs more thought, and it appears I do not +# need the base class. + +class SequenceVarExpression(BooleanExpression): + pass + +class NoOverlapExpression(SequenceVarExpression): + """ + Expression representing that none of the IntervalVars in a SequenceVar overlap + (if they are scheduled) + + args: + args (tuple): Child node of type SequenceVar + """ + def nargs(self): + return 1 + + def _to_string(self, values, verbose, smap): + return "no_overlap(%s)" % values[0] + + +class FirstInSequenceExpression(SequenceVarExpression): + """ + Expression representing that the specified IntervalVar is the first in the + sequence specified by SequenceVar (if it is scheduled) + + args: + args (tuple): Child nodes, the first of type IntervalVar, the second of type + SequenceVar + """ + def nargs(self): + return 2 + + def _to_string(self, values, verbose, smap): + return "first_in(%s, %s)" % (values[0], values[1]) + + +class LastInSequenceExpression(SequenceVarExpression): + """ + Expression representing that the specified IntervalVar is the last in the + sequence specified by SequenceVar (if it is scheduled) + + args: + args (tuple): Child nodes, the first of type IntervalVar, the second of type + SequenceVar + """ + def nargs(self): + return 2 + + def _to_string(self, values, verbose, smap): + return "last_in(%s, %s)" % (values[0], values[1]) + + +class BeforeInSequenceExpression(SequenceVarExpression): + """ + Expression representing that one IntervalVar occurs before another in the + sequence specified by the given SequenceVar (if both are scheduled) + + args: + args (tuple): Child nodes, the IntervalVar that must be before, the + IntervalVar that must be after, and the SequenceVar + """ + def nargs(self): + return 3 + + def _to_string(self, values, verbose, smap): + return "before_in(%s, %s, %s)" % (values[0], values[1], values[2]) + + +class PredecessorToExpression(SequenceVarExpression): + """ + Expression representing that one IntervalVar is a direct predecessor to another + in the sequence specified by the given SequenceVar (if both are scheduled) + + args: + args (tuple): Child nodes, the predecessor IntervalVar, the successor + IntervalVar, and the SequenceVar + """ + def nargs(self): + return 3 + + def _to_string(self, values, verbose, smap): + return "predecessor_to(%s, %s, %s)" % (values[0], values[1], values[2]) + + +def no_overlap(sequence_var): + """ + Creates a new NoOverlapExpression + + Requires that none of the scheduled intervals in the SequenceVar overlap each other + + args: + sequence_var: A SequenceVar + """ + return NoOverlapExpression((sequence_var,)) + + +def first_in_sequence(interval_var, sequence_var): + """ + Creates a new FirstInSequenceExpression + + Requires that 'interval_var' be the first in the sequence specified by + 'sequence_var' if it is scheduled + + args: + interval_var (IntervalVar): The activity that should be scheduled first + if it is scheduled at all + sequence_var (SequenceVar): The sequence of activities + """ + return FirstInSequenceExpression((interval_var, sequence_var,)) + + +def last_in_sequence(interval_var, sequence_var): + """ + Creates a new LastInSequenceExpression + + Requires that 'interval_var' be the last in the sequence specified by + 'sequence_var' if it is scheduled + + args: + interval_var (IntervalVar): The activity that should be scheduled last + if it is scheduled at all + sequence_var (SequenceVar): The sequence of activities + """ + + return LastInSequenceExpression((interval_var, sequence_var,)) + + +def before_in_sequence(before_var, after_var, sequence_var): + """ + Creates a new BeforeInSequenceExpression + + Requires that 'before_var' be scheduled to start before 'after_var' in the + sequence spcified bv 'sequence_var', if both are scheduled + + args: + before_var (IntervalVar): The activity that should be scheduled earlier in + the sequence + after_var (IntervalVar): The activity that should be scheduled later in the + sequence + sequence_var (SequenceVar): The sequence of activities + """ + return BeforeInSequenceExpression((before_var, after_var, sequence_var,)) + + +def predecessor_to(before_var, after_var, sequence_var): + """ + Creates a new PredecessorToExpression + + Requires that 'before_var' be a direct predecessor to 'after_var' in the + sequence specified by 'sequence_var', if both are scheduled + + args: + before_var (IntervalVar): The activity that should be scheduled as the + predecessor + after_var (IntervalVar): The activity that should be scheduled as the + successor + sequence_var (SequenceVar): The sequence of activities + """ + return PredecessorToExpression((before_var, after_var, sequence_var,)) diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py new file mode 100644 index 00000000000..a35eb9b67af --- /dev/null +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -0,0 +1,104 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from io import StringIO +import pyomo.common.unittest as unittest +from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + NoOverlapExpression, + FirstInSequenceExpression, + LastInSequenceExpression, + BeforeInSequenceExpression, + PredecessorToExpression, + no_overlap, + predecessor_to, + before_in_sequence, + first_in_sequence, + last_in_sequence, +) +from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar +from pyomo.environ import ConcreteModel, Integers, LogicalConstraint, Set, value, Var + + +class TestSequenceVarExpressions(unittest.TestCase): + def get_model(self): + m = ConcreteModel() + m.S = Set(initialize=range(3)) + m.i = IntervalVar(m.S, start=(0, 5)) + m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + + return m + + def test_no_overlap(self): + m = self.get_model() + m.c = LogicalConstraint(expr=no_overlap(m.seq)) + e = m.c.expr + + self.assertIsInstance(e, NoOverlapExpression) + self.assertEqual(e.nargs(), 1) + self.assertEqual(len(e.args), 1) + self.assertIs(e.args[0], m.seq) + + self.assertEqual(str(e), "no_overlap(seq)") + + def test_first_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=first_in_sequence(m.i[2], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, FirstInSequenceExpression) + self.assertEqual(e.nargs(), 2) + self.assertEqual(len(e.args), 2) + self.assertIs(e.args[0], m.i[2]) + self.assertIs(e.args[1], m.seq) + + self.assertEqual(str(e), "first_in(i[2], seq)") + + def test_last_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=last_in_sequence(m.i[0], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, LastInSequenceExpression) + self.assertEqual(e.nargs(), 2) + self.assertEqual(len(e.args), 2) + self.assertIs(e.args[0], m.i[0]) + self.assertIs(e.args[1], m.seq) + + self.assertEqual(str(e), "last_in(i[0], seq)") + + def test_before_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=before_in_sequence(m.i[1], m.i[0], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, BeforeInSequenceExpression) + self.assertEqual(e.nargs(), 3) + self.assertEqual(len(e.args), 3) + self.assertIs(e.args[0], m.i[1]) + self.assertIs(e.args[1], m.i[0]) + self.assertIs(e.args[2], m.seq) + + self.assertEqual(str(e), "before_in(i[1], i[0], seq)") + + def test_predecessor_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=predecessor_to(m.i[0], m.i[1], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, PredecessorToExpression) + self.assertEqual(e.nargs(), 3) + self.assertEqual(len(e.args), 3) + self.assertIs(e.args[0], m.i[0]) + self.assertIs(e.args[1], m.i[1]) + self.assertIs(e.args[2], m.seq) + + self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") From 05d9020e292b2d3013b532a566f86addba6b22d3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 15:12:53 -0700 Subject: [PATCH 0150/1178] Adding handling for sequence var expressions in the docplex writer --- pyomo/contrib/cp/repn/docplex_writer.py | 83 +++++++++++++++++++ pyomo/contrib/cp/tests/test_docplex_walker.py | 78 ++++++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 50a2d72aed8..38e2a0ae94f 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -30,10 +30,22 @@ IntervalVarData, IndexedIntervalVar, ) +from pyomo.contrib.cp.sequence_var import( + ScalarSequenceVar, + IndexedSequenceVar, + _SequenceVarData, +) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, ) +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + NoOverlapExpression, + FirstInSequenceExpression, + LastInSequenceExpression, + BeforeInSequenceExpression, + PredecessorToExpression, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, StepAt, @@ -491,6 +503,16 @@ def _create_docplex_interval_var(visitor, interval_var): return cpx_interval_var +def _create_docplex_sequence_var(visitor, sequence_var): + nm = sequence_var.name if visitor.symbolic_solver_labels else None + + cpx_seq_var = cp.sequence_var(name=nm, + vars=[_get_docplex_interval_var(visitor, v) + for v in sequence_var.interval_vars]) + visitor.var_map[id(sequence_var)] = cpx_seq_var + return cpx_seq_var + + def _get_docplex_interval_var(visitor, interval_var): # We might already have the interval_var and just need to retrieve it if id(interval_var) in visitor.var_map: @@ -501,6 +523,37 @@ def _get_docplex_interval_var(visitor, interval_var): return cpx_interval_var +def _get_docplex_sequence_var(visitor, sequence_var): + if id(sequence_var) in visitor.var_map: + cpx_seq_var = visitor.var_map[id(sequence_var)] + else: + cpx_seq_var = _create_docplex_sequence_var(visitor, sequence_var) + visitor.cpx.add(cpx_seq_var) + return cpx_seq_var + + +def _before_sequence_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + cpx_seq_var = _get_docplex_sequence_var(visitor, child) + visitor.var_map[_id] = cpx_seq_var + visitor.pyomo_to_docplex[child] = cpx_seq_var + + return False, (_GENERAL, visitor.var_map[_id]) + + +def _before_indexed_sequence_var(visitor, child): + # ESJ TODO: I'm not sure we can encounter an indexed sequence var in an + # expression right now? + cpx_vars = {} + for i, v in child.items(): + cpx_sequence_var = _get_docplex_sequence_var(visitor, v) + visitor.var_map[id(v)] = cpx_sequence_var + visitor.pyomo_to_docplex[v] = cpx_sequence_var + cpx_vars[i] = cpx_sequence_var + return False, (_GENERAL, cpx_vars) + + def _before_interval_var(visitor, child): _id = id(child) if _id not in visitor.var_map: @@ -902,6 +955,28 @@ def _handle_always_in_node(visitor, node, cumul_func, lb, ub, start, end): ) +def _handle_no_overlap_expression_node(visitor, node, seq_var): + return _GENERAL, cp.no_overlap(seq_var[1]) + + +def _handle_first_in_sequence_expression_node(visitor, node, interval_var, seq_var): + return _GENERAL, cp.first(seq_var[1], interval_var[1]) + + +def _handle_last_in_sequence_expression_node(visitor, node, interval_var, seq_var): + return _GENERAL, cp.last(seq_var[1], interval_var[1]) + + +def _handle_before_in_sequence_expression_node(visitor, node, before_var, + after_var, seq_var): + return _GENERAL, cp.before(seq_var[1], before_var[1], after_var[1]) + + +def _handle_predecessor_to_expression_node(visitor, node, before_var, after_var, + seq_var): + return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) + + class LogicalToDoCplex(StreamBasedExpressionVisitor): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, @@ -941,6 +1016,11 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): AlwaysIn: _handle_always_in_node, _GeneralExpressionData: _handle_named_expression_node, ScalarExpression: _handle_named_expression_node, + NoOverlapExpression: _handle_no_overlap_expression_node, + FirstInSequenceExpression: _handle_first_in_sequence_expression_node, + LastInSequenceExpression: _handle_last_in_sequence_expression_node, + BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, + PredecessorToExpression: _handle_predecessor_to_expression_node, } _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, @@ -950,6 +1030,9 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarIntervalVar: _before_interval_var, IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, + ScalarSequenceVar: _before_sequence_var, + _SequenceVarData: _before_sequence_var, + IndexedSequenceVar: _before_indexed_sequence_var, ScalarVar: _before_var, _GeneralVarData: _before_var, IndexedVar: _before_indexed_var, diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 97bc538c827..dc35a60050c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -11,7 +11,15 @@ import pyomo.common.unittest as unittest -from pyomo.contrib.cp import IntervalVar +from pyomo.contrib.cp import ( + IntervalVar, + SequenceVar, + no_overlap, + first_in_sequence, + last_in_sequence, + before_in_sequence, + predecessor_to, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, @@ -769,6 +777,74 @@ def test_interval_var_fixed_start_and_end(self): self.assertEqual(i.get_end(), (6, 6)) +@unittest.skipIf(not docplex_available, "docplex is not available") +class TestCPExpressionWalker_SequenceVars(CommonTest): + def get_model(self): + m = super().get_model() + m.seq = SequenceVar(expr=[m.i, m.i2[1], m.i2[2]]) + + return m + + def check_scalar_sequence_var(self, m, visitor): + self.assertIn(id(m.seq), visitor.var_map) + seq = visitor.var_map[id(m.seq)] + + i = visitor.var_map[id(m.i)] + i21 = visitor.var_map[id(m.i2[1])] + i22 = visitor.var_map[id(m.i2[2])] + + ivs = seq.get_interval_variables() + self.assertEqual(len(ivs), 3) + self.assertIs(ivs[0], i) + self.assertIs(ivs[1], i21) + self.assertIs(ivs[2], i22) + + return seq, i, i21, i22 + + def test_scalar_sequence_var(self): + m = self.get_model() + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.seq, m.seq, 0)) + self.check_scalar_sequence_var(m, visitor) + + def test_no_overlap(self): + m = self.get_model() + e = no_overlap(m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.no_overlap(seq))) + + def test_first_in_sequence(self): + m = self.get_model() + e = first_in_sequence(m.i2[1], m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.first(seq, i21))) + + def test_before_in_sequence(self): + m = self.get_model() + e = last_in_sequence(m.i, m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.last(seq, i))) + + def test_last_in_sequence(self): + m = self.get_model() + e = last_in_sequence(m.i2[1], m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.last(seq, i21))) + + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_PrecedenceExpressions(CommonTest): def test_start_before_start(self): From 1cbadc533368a1394a229dfee2bfe759953c96f3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 15:15:31 -0700 Subject: [PATCH 0151/1178] Blackify --- pyomo/contrib/cp/repn/docplex_writer.py | 21 ++++++++++------- .../scheduling_expr/sequence_expressions.py | 23 ++++++++++++------- pyomo/contrib/cp/sequence_var.py | 16 +++++++------ .../cp/tests/test_sequence_expressions.py | 6 ++--- pyomo/contrib/cp/tests/test_sequence_var.py | 20 ++++++++-------- 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 38e2a0ae94f..fb816240c1e 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -30,7 +30,7 @@ IntervalVarData, IndexedIntervalVar, ) -from pyomo.contrib.cp.sequence_var import( +from pyomo.contrib.cp.sequence_var import ( ScalarSequenceVar, IndexedSequenceVar, _SequenceVarData, @@ -506,9 +506,12 @@ def _create_docplex_interval_var(visitor, interval_var): def _create_docplex_sequence_var(visitor, sequence_var): nm = sequence_var.name if visitor.symbolic_solver_labels else None - cpx_seq_var = cp.sequence_var(name=nm, - vars=[_get_docplex_interval_var(visitor, v) - for v in sequence_var.interval_vars]) + cpx_seq_var = cp.sequence_var( + name=nm, + vars=[ + _get_docplex_interval_var(visitor, v) for v in sequence_var.interval_vars + ], + ) visitor.var_map[id(sequence_var)] = cpx_seq_var return cpx_seq_var @@ -967,13 +970,15 @@ def _handle_last_in_sequence_expression_node(visitor, node, interval_var, seq_va return _GENERAL, cp.last(seq_var[1], interval_var[1]) -def _handle_before_in_sequence_expression_node(visitor, node, before_var, - after_var, seq_var): +def _handle_before_in_sequence_expression_node( + visitor, node, before_var, after_var, seq_var +): return _GENERAL, cp.before(seq_var[1], before_var[1], after_var[1]) -def _handle_predecessor_to_expression_node(visitor, node, before_var, after_var, - seq_var): +def _handle_predecessor_to_expression_node( + visitor, node, before_var, after_var, seq_var +): return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index 0a49198c1d1..b39322322da 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -14,9 +14,11 @@ # ESJ TODO: The naming in this file needs more thought, and it appears I do not # need the base class. + class SequenceVarExpression(BooleanExpression): pass + class NoOverlapExpression(SequenceVarExpression): """ Expression representing that none of the IntervalVars in a SequenceVar overlap @@ -25,6 +27,7 @@ class NoOverlapExpression(SequenceVarExpression): args: args (tuple): Child node of type SequenceVar """ + def nargs(self): return 1 @@ -41,6 +44,7 @@ class FirstInSequenceExpression(SequenceVarExpression): args (tuple): Child nodes, the first of type IntervalVar, the second of type SequenceVar """ + def nargs(self): return 2 @@ -57,6 +61,7 @@ class LastInSequenceExpression(SequenceVarExpression): args (tuple): Child nodes, the first of type IntervalVar, the second of type SequenceVar """ + def nargs(self): return 2 @@ -70,9 +75,10 @@ class BeforeInSequenceExpression(SequenceVarExpression): sequence specified by the given SequenceVar (if both are scheduled) args: - args (tuple): Child nodes, the IntervalVar that must be before, the + args (tuple): Child nodes, the IntervalVar that must be before, the IntervalVar that must be after, and the SequenceVar """ + def nargs(self): return 3 @@ -86,9 +92,10 @@ class PredecessorToExpression(SequenceVarExpression): in the sequence specified by the given SequenceVar (if both are scheduled) args: - args (tuple): Child nodes, the predecessor IntervalVar, the successor + args (tuple): Child nodes, the predecessor IntervalVar, the successor IntervalVar, and the SequenceVar """ + def nargs(self): return 3 @@ -120,7 +127,7 @@ def first_in_sequence(interval_var, sequence_var): if it is scheduled at all sequence_var (SequenceVar): The sequence of activities """ - return FirstInSequenceExpression((interval_var, sequence_var,)) + return FirstInSequenceExpression((interval_var, sequence_var)) def last_in_sequence(interval_var, sequence_var): @@ -136,24 +143,24 @@ def last_in_sequence(interval_var, sequence_var): sequence_var (SequenceVar): The sequence of activities """ - return LastInSequenceExpression((interval_var, sequence_var,)) + return LastInSequenceExpression((interval_var, sequence_var)) def before_in_sequence(before_var, after_var, sequence_var): """ Creates a new BeforeInSequenceExpression - Requires that 'before_var' be scheduled to start before 'after_var' in the + Requires that 'before_var' be scheduled to start before 'after_var' in the sequence spcified bv 'sequence_var', if both are scheduled args: - before_var (IntervalVar): The activity that should be scheduled earlier in + before_var (IntervalVar): The activity that should be scheduled earlier in the sequence after_var (IntervalVar): The activity that should be scheduled later in the sequence sequence_var (SequenceVar): The sequence of activities """ - return BeforeInSequenceExpression((before_var, after_var, sequence_var,)) + return BeforeInSequenceExpression((before_var, after_var, sequence_var)) def predecessor_to(before_var, after_var, sequence_var): @@ -170,4 +177,4 @@ def predecessor_to(before_var, after_var, sequence_var): successor sequence_var (SequenceVar): The sequence of activities """ - return PredecessorToExpression((before_var, after_var, sequence_var,)) + return PredecessorToExpression((before_var, after_var, sequence_var)) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index b0691fbd74a..a77f4c2c415 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -27,7 +27,9 @@ class _SequenceVarData(ActiveComponentData): """This class defines the abstract interface for a single sequence variable.""" + __slots__ = ('interval_vars',) + def __init__(self, component=None): # in-lining ActiveComponentData and ComponentData constructors, as is # traditional: @@ -45,14 +47,15 @@ def set_value(self, expr): if not hasattr(expr, '__iter__'): raise ValueError( "'expr' for SequenceVar must be a list of IntervalVars. " - "Encountered type '%s' constructing '%s'" % (type(expr), - self.name)) + "Encountered type '%s' constructing '%s'" % (type(expr), self.name) + ) for v in expr: if not hasattr(v, 'ctype') or v.ctype is not IntervalVar: raise ValueError( "The SequenceVar 'expr' argument must be a list of " "IntervalVars. The 'expr' for SequenceVar '%s' included " - "an object of type '%s'" % (self.name, type(v))) + "an object of type '%s'" % (self.name, type(v)) + ) self.interval_vars.append(v) @@ -101,7 +104,7 @@ def construct(self, data=None): if self._constructed: return self._constructed = True - + if is_debug_set(logger): logger.debug("Constructing SequenceVar %s" % self.name) @@ -132,11 +135,10 @@ def _pprint(self): headers, self._data.items(), ("IntervalVars",), - lambda k, v: [ - '[' + ', '.join(iv.name for iv in v.interval_vars) + ']', - ] + lambda k, v: ['[' + ', '.join(iv.name for iv in v.interval_vars) + ']'], ) + class ScalarSequenceVar(_SequenceVarData, SequenceVar): def __init__(self, *args, **kwds): _SequenceVarData.__init__(self, component=self) diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index a35eb9b67af..0ef2a9e3072 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -74,7 +74,7 @@ def test_last_in_sequence(self): self.assertIs(e.args[1], m.seq) self.assertEqual(str(e), "last_in(i[0], seq)") - + def test_before_in_sequence(self): m = self.get_model() m.c = LogicalConstraint(expr=before_in_sequence(m.i[1], m.i[0], m.seq)) @@ -93,12 +93,12 @@ def test_predecessor_in_sequence(self): m = self.get_model() m.c = LogicalConstraint(expr=predecessor_to(m.i[0], m.i[1], m.seq)) e = m.c.expr - + self.assertIsInstance(e, PredecessorToExpression) self.assertEqual(e.nargs(), 3) self.assertEqual(len(e.args), 3) self.assertIs(e.args[0], m.i[0]) self.assertIs(e.args[1], m.i[1]) self.assertIs(e.args[2], m.seq) - + self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 852d9f2134a..385ad2dd7ec 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -58,16 +58,16 @@ def test_pprint(self): seq : Size=1, Index=None Key : IntervalVars None : [i[0], i[1], i[2]] - """.strip() + """.strip(), ) def test_interval_vars_not_a_list(self): m = self.get_model() with self.assertRaisesRegex( - ValueError, - "'expr' for SequenceVar must be a list of IntervalVars. " - "Encountered type '' constructing 'seq2'" + ValueError, + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '' constructing 'seq2'", ): m.seq2 = SequenceVar(expr=1) @@ -75,13 +75,14 @@ def test_interval_vars_list_includes_things_that_are_not_interval_vars(self): m = self.get_model() with self.assertRaisesRegex( - ValueError, - "The SequenceVar 'expr' argument must be a list of " - "IntervalVars. The 'expr' for SequenceVar 'seq2' included " - "an object of type ''" + ValueError, + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar 'seq2' included " + "an object of type ''", ): m.seq2 = SequenceVar(expr=m.i) + class TestIndexedSequenceVar(unittest.TestCase): def test_initialize_with_not_data(self): m = ConcreteModel() @@ -110,6 +111,7 @@ def make_model(self): def the_rule(m, j): return [m.i[j, k] for k in m.num] + m.seq = SequenceVar(m.alph, rule=the_rule) return m @@ -137,5 +139,5 @@ def test_pprint(self): seq : Size=2, Index=alph Key : IntervalVars a : [i[a,1], i[a,2]] - b : [i[b,1], i[b,2]]""".strip() + b : [i[b,1], i[b,2]]""".strip(), ) From 70a4c5ef956140566b992e95149bf064e4ffb4e3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 17:10:23 -0700 Subject: [PATCH 0152/1178] Fixing a typo --- pyomo/contrib/cp/scheduling_expr/sequence_expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index b39322322da..d88504ac7e4 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -151,7 +151,7 @@ def before_in_sequence(before_var, after_var, sequence_var): Creates a new BeforeInSequenceExpression Requires that 'before_var' be scheduled to start before 'after_var' in the - sequence spcified bv 'sequence_var', if both are scheduled + sequence specified bv 'sequence_var', if both are scheduled args: before_var (IntervalVar): The activity that should be scheduled earlier in From f1ad1d0ecead8e42f8bdf289f21507224a1f1e5e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 17:11:40 -0700 Subject: [PATCH 0153/1178] The linter won't let me name something 'alph' --- pyomo/contrib/cp/tests/test_sequence_var.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 385ad2dd7ec..8167c9f5b3b 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -105,14 +105,14 @@ def test_initialize_with_not_data(self): def make_model(self): m = ConcreteModel() - m.alph = Set(initialize=['a', 'b']) - m.num = Set(initialize=[1, 2]) - m.i = IntervalVar(m.alph, m.num) + m.alphabetic = Set(initialize=['a', 'b']) + m.numeric = Set(initialize=[1, 2]) + m.i = IntervalVar(m.alphabetic, m.numeric) def the_rule(m, j): - return [m.i[j, k] for k in m.num] + return [m.i[j, k] for k in m.numeric] - m.seq = SequenceVar(m.alph, rule=the_rule) + m.seq = SequenceVar(m.alphabetic, rule=the_rule) return m @@ -121,10 +121,10 @@ def test_initialize_with_rule(self): self.assertIsInstance(m.seq, IndexedSequenceVar) self.assertEqual(len(m.seq), 2) - for j in m.alph: + for j in m.alphabetic: self.assertTrue(j in m.seq) self.assertEqual(len(m.seq[j].interval_vars), 2) - for k in m.num: + for k in m.numeric: self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) def test_pprint(self): @@ -136,7 +136,7 @@ def test_pprint(self): self.assertEqual( buf.getvalue().strip(), """ -seq : Size=2, Index=alph +seq : Size=2, Index=alphabetic Key : IntervalVars a : [i[a,1], i[a,2]] b : [i[b,1], i[b,2]]""".strip(), From 4354754edb50c88d1671ecd7678237821455eeab Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 13 Dec 2023 12:48:31 -0700 Subject: [PATCH 0154/1178] Adding a test for multidimensional indexed sequence vars --- pyomo/contrib/cp/tests/test_sequence_var.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 8167c9f5b3b..37190ebca89 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -141,3 +141,17 @@ def test_pprint(self): a : [i[a,1], i[a,2]] b : [i[b,1], i[b,2]]""".strip(), ) + + def test_multidimensional_index(self): + m = self.make_model() + @m.SequenceVar(m.alphabetic, m.numeric) + def s(m, i, j): + return [m.i[i, j],] + + self.assertIsInstance(m.s, IndexedSequenceVar) + self.assertEqual(len(m.s), 4) + for i in m.alphabetic: + for j in m.numeric: + self.assertTrue((i, j) in m.s) + self.assertEqual(len(m.s[i, j]), 1) + self.assertIs(m.s[i, j].interval_vars[0], m.i[i, j]) From 5d7b74036d188845e3653c848f179acb2d44936f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 08:53:59 -0700 Subject: [PATCH 0155/1178] Begin cleaning up documentation; move tests to more logical configuration --- pyomo/solver/base.py | 67 ++++++++++++++----- pyomo/solver/tests/{ => unit}/test_base.py | 0 pyomo/solver/tests/{ => unit}/test_config.py | 0 pyomo/solver/tests/{ => unit}/test_results.py | 0 .../solver/tests/{ => unit}/test_solution.py | 0 pyomo/solver/tests/{ => unit}/test_util.py | 0 6 files changed, 52 insertions(+), 15 deletions(-) rename pyomo/solver/tests/{ => unit}/test_base.py (100%) rename pyomo/solver/tests/{ => unit}/test_config.py (100%) rename pyomo/solver/tests/{ => unit}/test_results.py (100%) rename pyomo/solver/tests/{ => unit}/test_solution.py (100%) rename pyomo/solver/tests/{ => unit}/test_util.py (100%) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index d4b46ebe5d4..fc361bdaf5e 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -37,6 +37,18 @@ class SolverBase(abc.ABC): + """ + Base class upon which direct solver interfaces can be built. + + This base class contains the required methods for all direct solvers: + - available: Determines whether the solver is able to be run, combining + both whether it can be found on the system and if the license is valid. + - config: The configuration method for solver objects. + - solve: The main method of every solver + - version: The version of the solver + - is_persistent: Set to false for all direct solvers. + """ + # # Support "with" statements. Forgetting to call deactivate # on Plugins is a common source of memory leaks @@ -45,9 +57,14 @@ def __enter__(self): return self def __exit__(self, t, v, traceback): - pass + """Exit statement - enables `with` statements.""" class Availability(enum.IntEnum): + """ + Class to capture different statuses in which a solver can exist in + order to record its availability for use. + """ + FullLicense = 2 LimitedLicense = 1 NotFound = 0 @@ -56,7 +73,7 @@ class Availability(enum.IntEnum): NeedsCompiledExtension = -3 def __bool__(self): - return self._value_ > 0 + return self.real > 0 def __format__(self, format_spec): # We want general formatting of this Enum to return the @@ -93,7 +110,6 @@ def solve( results: Results A results object """ - pass @abc.abstractmethod def available(self): @@ -120,7 +136,6 @@ def available(self): be True if the solver is runable at all and False otherwise. """ - pass @abc.abstractmethod def version(self) -> Tuple: @@ -143,7 +158,6 @@ def config(self): An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ - pass def is_persistent(self): """ @@ -156,7 +170,21 @@ def is_persistent(self): class PersistentSolverBase(SolverBase): + """ + Base class upon which persistent solvers can be built. This inherits the + methods from the direct solver base and adds those methods that are necessary + for persistent solvers. + + Example usage can be seen in solvers within APPSI. + """ + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ return True def load_vars( @@ -179,7 +207,20 @@ def load_vars( def get_primals( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: - pass + """ + Get mapping of variables to primals. + + Parameters + ---------- + vars_to_load : Optional[Sequence[_GeneralVarData]], optional + Which vars to be populated into the map. The default is None. + + Returns + ------- + Mapping[_GeneralVarData, float] + A map of variables to primals. + + """ def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -198,9 +239,6 @@ def get_duals( duals: dict Maps constraints to dual values """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) def get_slacks( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -217,9 +255,6 @@ def get_slacks( slacks: dict Maps constraints to slack values """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -236,9 +271,6 @@ def get_reduced_costs( reduced_costs: ComponentMap Maps variable to reduced cost """ - raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) - ) @property @abc.abstractmethod @@ -295,6 +327,11 @@ def update_params(self): class LegacySolverInterface: + """ + Class to map the new solver interface features into the legacy solver + interface. Necessary for backwards compatibility. + """ + def solve( self, model: _BlockData, diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/unit/test_base.py similarity index 100% rename from pyomo/solver/tests/test_base.py rename to pyomo/solver/tests/unit/test_base.py diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/unit/test_config.py similarity index 100% rename from pyomo/solver/tests/test_config.py rename to pyomo/solver/tests/unit/test_config.py diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/unit/test_results.py similarity index 100% rename from pyomo/solver/tests/test_results.py rename to pyomo/solver/tests/unit/test_results.py diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/unit/test_solution.py similarity index 100% rename from pyomo/solver/tests/test_solution.py rename to pyomo/solver/tests/unit/test_solution.py diff --git a/pyomo/solver/tests/test_util.py b/pyomo/solver/tests/unit/test_util.py similarity index 100% rename from pyomo/solver/tests/test_util.py rename to pyomo/solver/tests/unit/test_util.py From 2dee6b7fee8cf394c1502b83e4989217ad95413f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 09:29:04 -0700 Subject: [PATCH 0156/1178] Reinstantiate NotImplementedErrors for PersistentBase; update tests --- pyomo/solver/base.py | 106 +++++++++++++++++++++------ pyomo/solver/tests/unit/test_base.py | 4 +- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index fc361bdaf5e..c025e2028ce 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -73,7 +73,7 @@ class Availability(enum.IntEnum): NeedsCompiledExtension = -3 def __bool__(self): - return self.real > 0 + return self._value_ > 0 def __format__(self, format_spec): # We want general formatting of this Enum to return the @@ -219,8 +219,10 @@ def get_primals( ------- Mapping[_GeneralVarData, float] A map of variables to primals. - """ + raise NotImplementedError( + '{0} does not support the get_primals method'.format(type(self)) + ) def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -239,6 +241,9 @@ def get_duals( duals: dict Maps constraints to dual values """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) def get_slacks( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -255,6 +260,9 @@ def get_slacks( slacks: dict Maps constraints to slack values """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -271,59 +279,88 @@ def get_reduced_costs( reduced_costs: ComponentMap Maps variable to reduced cost """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) @property @abc.abstractmethod def update_config(self) -> UpdateConfig: - pass + """ + Updates the solver config + """ @abc.abstractmethod def set_instance(self, model): - pass + """ + Set an instance of the model + """ @abc.abstractmethod def add_variables(self, variables: List[_GeneralVarData]): - pass + """ + Add variables to the model + """ @abc.abstractmethod def add_params(self, params: List[_ParamData]): - pass + """ + Add parameters to the model + """ @abc.abstractmethod def add_constraints(self, cons: List[_GeneralConstraintData]): - pass + """ + Add constraints to the model + """ @abc.abstractmethod def add_block(self, block: _BlockData): - pass + """ + Add a block to the model + """ @abc.abstractmethod def remove_variables(self, variables: List[_GeneralVarData]): - pass + """ + Remove variables from the model + """ @abc.abstractmethod def remove_params(self, params: List[_ParamData]): - pass + """ + Remove parameters from the model + """ @abc.abstractmethod def remove_constraints(self, cons: List[_GeneralConstraintData]): - pass + """ + Remove constraints from the model + """ @abc.abstractmethod def remove_block(self, block: _BlockData): - pass + """ + Remove a block from the model + """ @abc.abstractmethod def set_objective(self, obj: _GeneralObjectiveData): - pass + """ + Set current objective for the model + """ @abc.abstractmethod def update_variables(self, variables: List[_GeneralVarData]): - pass + """ + Update variables on the model + """ @abc.abstractmethod def update_params(self): - pass + """ + Update parameters on the model + """ class LegacySolverInterface: @@ -332,6 +369,10 @@ class LegacySolverInterface: interface. Necessary for backwards compatibility. """ + def __init__(self): + self.original_config = self.config + self.config = self.config() + def solve( self, model: _BlockData, @@ -347,7 +388,15 @@ def solve( keepfiles: bool = False, symbolic_solver_labels: bool = False, ): - original_config = self.config + """ + Solve method: maps new solve method style to backwards compatible version. + + Returns + ------- + legacy_results + Legacy results object + + """ self.config = self.config() self.config.tee = tee self.config.load_solution = load_solutions @@ -401,10 +450,10 @@ def solve( legacy_soln.gap = None symbol_map = SymbolMap() - symbol_map.byObject = dict(self.symbol_map.byObject) - symbol_map.bySymbol = dict(self.symbol_map.bySymbol) - symbol_map.aliases = dict(self.symbol_map.aliases) - symbol_map.default_labeler = self.symbol_map.default_labeler + symbol_map.byObject = dict(symbol_map.byObject) + symbol_map.bySymbol = dict(symbol_map.bySymbol) + symbol_map.aliases = dict(symbol_map.aliases) + symbol_map.default_labeler = symbol_map.default_labeler model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) @@ -439,12 +488,16 @@ def solve( if delete_legacy_soln: legacy_results.solution.delete(0) - self.config = original_config + self.config = self.original_config self.options = original_options return legacy_results def available(self, exception_flag=True): + """ + Returns a bool determining whether the requested solver is available + on the system. + """ ans = super().available() if exception_flag and not ans: raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') @@ -468,6 +521,14 @@ def license_is_valid(self) -> bool: @property def options(self): + """ + Read the options for the dictated solver. + + NOTE: Only the set of solvers for which the LegacySolverInterface is compatible + are accounted for within this property. + Not all solvers are currently covered by this backwards compatibility + class. + """ for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: if hasattr(self, solver_name + '_options'): return getattr(self, solver_name + '_options') @@ -475,6 +536,9 @@ def options(self): @options.setter def options(self, val): + """ + Set the options for the dictated solver. + """ found = False for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: if hasattr(self, solver_name + '_options'): diff --git a/pyomo/solver/tests/unit/test_base.py b/pyomo/solver/tests/unit/test_base.py index d8084e9b5b7..b501f8d3dd3 100644 --- a/pyomo/solver/tests/unit/test_base.py +++ b/pyomo/solver/tests/unit/test_base.py @@ -63,7 +63,6 @@ def test_abstract_member_list(self): def test_persistent_solver_base(self): self.instance = base.PersistentSolverBase() self.assertTrue(self.instance.is_persistent()) - self.assertEqual(self.instance.get_primals(), None) self.assertEqual(self.instance.update_config, None) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) @@ -78,6 +77,9 @@ def test_persistent_solver_base(self): self.assertEqual(self.instance.update_variables(None), None) self.assertEqual(self.instance.update_params(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_primals() + with self.assertRaises(NotImplementedError): self.instance.get_duals() From f0c0a07e42e05733d90d41b92db89f5e05194bd1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 09:36:09 -0700 Subject: [PATCH 0157/1178] Revert config changes --- pyomo/solver/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index c025e2028ce..1d459450bab 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -369,10 +369,6 @@ class LegacySolverInterface: interface. Necessary for backwards compatibility. """ - def __init__(self): - self.original_config = self.config - self.config = self.config() - def solve( self, model: _BlockData, @@ -397,6 +393,7 @@ def solve( Legacy results object """ + original_config = self.config self.config = self.config() self.config.tee = tee self.config.load_solution = load_solutions @@ -488,7 +485,7 @@ def solve( if delete_legacy_soln: legacy_results.solution.delete(0) - self.config = self.original_config + self.config = original_config self.options = original_options return legacy_results From 35e5ff0d5033ccb50b0e38a33c3f3d91c26626de Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 19 Dec 2023 10:57:46 -0700 Subject: [PATCH 0158/1178] bug fix --- pyomo/solver/util.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index ec59f7e80f7..c0c99a00747 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -173,7 +173,7 @@ def update_config(self, val: UpdateConfig): def set_instance(self, model): saved_update_config = self.update_config - self.__init__() + self.__init__(only_child_vars=self._only_child_vars) self.update_config = saved_update_config self._model = model self.add_block(model) @@ -632,17 +632,17 @@ def update(self, timer: HierarchicalTimer = None): vars_to_update = [] for v in vars_to_check: _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif (fixed is not v.fixed) or (fixed and (value != v.value)): + if (fixed != v.fixed) or (fixed and (value != v.value)): vars_to_update.append(v) if self.update_config.treat_fixed_vars_as_params: for c in self._referenced_variables[id(v)][0]: cons_to_remove_and_add[c] = None if self._referenced_variables[id(v)][2] is not None: need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) elif domain_interval != v.domain.get_interval(): vars_to_update.append(v) self.update_variables(vars_to_update) From 84896f39fd1dead0be2afda4e0894358c732786c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 11:18:02 -0700 Subject: [PATCH 0159/1178] Add descriptions; incorporate missing option into ipopt --- pyomo/solver/config.py | 69 +++++++++++++++++++++++++---------------- pyomo/solver/ipopt.py | 15 ++++++--- pyomo/solver/results.py | 6 ++++ 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 551f59ccd9a..54a497cee0c 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -21,24 +21,7 @@ class SolverConfig(ConfigDict): """ - Attributes - ---------- - time_limit: float - sent to solver - Time limit for the solver - tee: bool - If True, then the solver log goes to stdout - load_solution: bool - wrapper - If False, then the values of the primal variables will not be - loaded into the model - symbolic_solver_labels: bool - sent to solver - If True, the names given to the solver will reflect the names - of the pyomo components. Cannot be changed after set_instance - is called. - report_timing: bool - wrapper - If True, then some timing information will be printed at the - end of the solve. - threads: integer - sent to solver - Number of threads to be used by a solver. + Base config values for all solver interfaces """ def __init__( @@ -57,28 +40,62 @@ def __init__( visibility=visibility, ) - self.tee: bool = self.declare('tee', ConfigValue(domain=bool, default=False)) + self.tee: bool = self.declare( + 'tee', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver log prints to stdout.", + ), + ) self.load_solution: bool = self.declare( - 'load_solution', ConfigValue(domain=bool, default=True) + 'load_solution', + ConfigValue( + domain=bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), ) self.raise_exception_on_nonoptimal_result: bool = self.declare( 'raise_exception_on_nonoptimal_result', - ConfigValue(domain=bool, default=True), + ConfigValue( + domain=bool, + default=True, + description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + ), ) self.symbolic_solver_labels: bool = self.declare( - 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) + 'symbolic_solver_labels', + ConfigValue( + domain=bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + ), ) self.report_timing: bool = self.declare( - 'report_timing', ConfigValue(domain=bool, default=False) + 'report_timing', + ConfigValue( + domain=bool, + default=False, + description="If True, timing information will be printed at the end of a solve call.", + ), ) self.threads: Optional[int] = self.declare( - 'threads', ConfigValue(domain=NonNegativeInt) + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + ), ) self.time_limit: Optional[float] = self.declare( - 'time_limit', ConfigValue(domain=NonNegativeFloat) + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, description="Time limit applied to the solver." + ), ) self.solver_options: ConfigDict = self.declare( - 'solver_options', ConfigDict(implicit=True) + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), ) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 092b279269b..68ba4989ff4 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -48,8 +48,6 @@ class ipoptSolverError(PyomoException): General exception to catch solver system errors """ - pass - class ipoptConfig(SolverConfig): def __init__( @@ -84,6 +82,9 @@ def __init__( self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) + self.presolve: bool = self.declare( + 'presolve', ConfigValue(domain=bool, default=True) + ) class ipoptResults(Results): @@ -186,8 +187,6 @@ def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() self._writer.config.skip_trivial_constraints = True - # TODO: Make this an option; not always turned on - self._writer.config.linear_presolve = True self.ipopt_options = self._config.solver_options def available(self): @@ -265,6 +264,12 @@ def solve(self, model, **kwds): # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) + self._writer.config.linear_presolve = config.presolve + if config.threads: + logger.log( + logging.INFO, + msg="The `threads` option was utilized, but this has not yet been implemented for {self.__class__}.", + ) results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.temp_dir is None: @@ -405,6 +410,8 @@ def solve(self, model, **kwds): results.timing_info.wall_time = ( end_timestamp - start_timestamp ).total_seconds() + if config.report_timing: + results.report_timing() return results def _parse_ipopt_output(self, stream: io.StringIO): diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 0aa78bef6bc..ae909986ed0 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -246,6 +246,12 @@ def __str__(self): s += 'objective_bound: ' + str(self.objective_bound) return s + def report_timing(self): + print('Timing Information: ') + print('-' * 50) + self.timing_info.display() + print('-' * 50) + class ResultsReader: pass From c114ee3b991540485bd868415ce3a9ed25e73e7b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 16:17:37 -0700 Subject: [PATCH 0160/1178] Bug fix: Case where there is no active objective --- pyomo/contrib/trustregion/TRF.py | 2 +- pyomo/core/base/PyomoModel.py | 4 ++-- pyomo/solver/base.py | 17 +++++++++-------- pyomo/solver/ipopt.py | 10 +++++++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/trustregion/TRF.py b/pyomo/contrib/trustregion/TRF.py index 45e60df7658..24254599609 100644 --- a/pyomo/contrib/trustregion/TRF.py +++ b/pyomo/contrib/trustregion/TRF.py @@ -35,7 +35,7 @@ logger = logging.getLogger('pyomo.contrib.trustregion') -__version__ = '0.2.0' +__version__ = (0, 2, 0) def trust_region_method(model, decision_variables, ext_fcn_surrogate_map_rule, config): diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index 6aacabeb183..44bc5302217 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -789,7 +789,7 @@ def _load_model_data(self, modeldata, namespaces, **kwds): profile_memory = kwds.get('profile_memory', 0) if profile_memory >= 2 and pympler_available: - mem_used = pympler.muppy.get_size(muppy.get_objects()) + mem_used = pympler.muppy.get_size(pympler.muppy.get_objects()) print("") print( " Total memory = %d bytes prior to model " @@ -798,7 +798,7 @@ def _load_model_data(self, modeldata, namespaces, **kwds): if profile_memory >= 3: gc.collect() - mem_used = pympler.muppy.get_size(muppy.get_objects()) + mem_used = pympler.muppy.get_size(pympler.muppy.get_objects()) print( " Total memory = %d bytes prior to model " "construction (after garbage collection)" % mem_used diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 1d459450bab..72f63e0a1a0 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -430,14 +430,15 @@ def solve( legacy_results.solver.termination_message = str(results.termination_condition) obj = get_objective(model) - legacy_results.problem.sense = obj.sense - - if obj.sense == minimize: - legacy_results.problem.lower_bound = results.objective_bound - legacy_results.problem.upper_bound = results.incumbent_objective - else: - legacy_results.problem.upper_bound = results.objective_bound - legacy_results.problem.lower_bound = results.incumbent_objective + if obj: + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.objective_bound + legacy_results.problem.upper_bound = results.incumbent_objective + else: + legacy_results.problem.upper_bound = results.objective_bound + legacy_results.problem.lower_bound = results.incumbent_objective if ( results.incumbent_objective is not None and results.objective_bound is not None diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 68ba4989ff4..e85b726ba9b 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -20,6 +20,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.core.base import Objective from pyomo.core.base.label import NumericLabeler from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn from pyomo.solver.base import SolverBase, SymbolMap @@ -390,7 +391,14 @@ def solve(self, model, **kwds): ): model.rc.update(results.solution_loader.get_reduced_costs()) - if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: + if results.solution_status in { + SolutionStatus.feasible, + SolutionStatus.optimal, + } and len( + list( + model.component_data_objects(Objective, descend_into=True, active=True) + ) + ): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: From 13631448a72c4678de81838a86b73cd6a8142f6d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 20 Dec 2023 08:00:59 -0700 Subject: [PATCH 0161/1178] Bug fix: bcannot convert obj to bool --- pyomo/solver/base.py | 6 +++++- pyomo/solver/results.py | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 72f63e0a1a0..48adc44c4d7 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -369,6 +369,10 @@ class LegacySolverInterface: interface. Necessary for backwards compatibility. """ + def set_config(self, config): + # TODO: Make a mapping from new config -> old config + pass + def solve( self, model: _BlockData, @@ -430,7 +434,7 @@ def solve( legacy_results.solver.termination_message = str(results.termination_condition) obj = get_objective(model) - if obj: + if len(list(obj)) > 0: legacy_results.problem.sense = obj.sense if obj.sense == minimize: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index ae909986ed0..e99db52073b 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -40,8 +40,6 @@ class SolverResultsError(PyomoException): General exception to catch solver system errors """ - pass - class TerminationCondition(enum.Enum): """ From d5a2fba9cece7982d551be89e70737355e954ba2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 20 Dec 2023 15:17:59 -0700 Subject: [PATCH 0162/1178] Fix f-string warning --- pyomo/solver/ipopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index e85b726ba9b..1b4c0eb36cb 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -268,8 +268,8 @@ def solve(self, model, **kwds): self._writer.config.linear_presolve = config.presolve if config.threads: logger.log( - logging.INFO, - msg="The `threads` option was utilized, but this has not yet been implemented for {self.__class__}.", + logging.WARNING, + msg=f"The `threads` option was specified, but this has not yet been implemented for {self.__class__}.", ) results = ipoptResults() with TempfileManager.new_context() as tempfile: From d24286a3c2068772e6f900e5e843e70e2546a2a6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 14:10:53 -0700 Subject: [PATCH 0163/1178] Fixing a bug for multi-dimensionally indexed SequenceVars --- pyomo/contrib/cp/sequence_var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index a77f4c2c415..587106c8107 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -91,7 +91,7 @@ def _getitem_when_not_present(self, index): obj._index = index if self._init_rule is not None: - obj.set_value(self._init_rule(parent, index)) + obj.set_value(self._init_rule(parent, *index)) if self._init_expr is not None: obj.set_value(self._init_expr) From d5fcd687f16ca09e0e89f3eddad4a970abbaaaaf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 14:11:04 -0700 Subject: [PATCH 0164/1178] Fixing a typo in a test --- pyomo/contrib/cp/tests/test_sequence_var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 37190ebca89..e3d69153355 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -153,5 +153,5 @@ def s(m, i, j): for i in m.alphabetic: for j in m.numeric: self.assertTrue((i, j) in m.s) - self.assertEqual(len(m.s[i, j]), 1) + self.assertEqual(len(m.s[i, j].interval_vars), 1) self.assertIs(m.s[i, j].interval_vars[0], m.i[i, j]) From 0547fbc7081a98f0c4efb94853b848dbae7a1468 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 14:11:33 -0700 Subject: [PATCH 0165/1178] NFC: black --- pyomo/contrib/cp/tests/test_sequence_var.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index e3d69153355..404e21ca39c 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -144,9 +144,10 @@ def test_pprint(self): def test_multidimensional_index(self): m = self.make_model() + @m.SequenceVar(m.alphabetic, m.numeric) def s(m, i, j): - return [m.i[i, j],] + return [m.i[i, j]] self.assertIsInstance(m.s, IndexedSequenceVar) self.assertEqual(len(m.s), 4) From 17f4837e1cddd77e4dee31cc5201c810d0e5dec4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 15:27:33 -0700 Subject: [PATCH 0166/1178] Remembering that initializers exist --- pyomo/contrib/cp/sequence_var.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 587106c8107..b242b362f9d 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -18,6 +18,7 @@ from pyomo.core.base.component import ActiveComponentData from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import ActiveIndexedComponent +from pyomo.core.base.initializer import Initializer import sys from weakref import ref as weakref_ref @@ -72,7 +73,7 @@ def __new__(cls, *args, **kwds): return IndexedSequenceVar.__new__(IndexedSequenceVar) def __init__(self, *args, **kwargs): - self._init_rule = kwargs.pop('rule', None) + self._init_rule = Initializer(kwargs.pop('rule', None)) self._init_expr = kwargs.pop('expr', None) kwargs.setdefault('ctype', SequenceVar) super(SequenceVar, self).__init__(*args, **kwargs) @@ -91,7 +92,7 @@ def _getitem_when_not_present(self, index): obj._index = index if self._init_rule is not None: - obj.set_value(self._init_rule(parent, *index)) + obj.set_value(self._init_rule(parent, index)) if self._init_expr is not None: obj.set_value(self._init_expr) From 687f754c1073e943b7d7280ad653a2d81328f8f5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 16:03:49 -0700 Subject: [PATCH 0167/1178] Fixing some bugs with SequenceVars in the writer and solution parsing --- pyomo/contrib/cp/repn/docplex_writer.py | 8 +++++- pyomo/contrib/cp/tests/test_docplex_writer.py | 27 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 002039b46dd..b1b8708c757 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -31,6 +31,7 @@ IndexedIntervalVar, ) from pyomo.contrib.cp.sequence_var import ( + SequenceVar, ScalarSequenceVar, IndexedSequenceVar, _SequenceVarData, @@ -1157,7 +1158,8 @@ def write(self, model, **options): RangeSet, Port, }, - targets={Objective, Constraint, LogicalConstraint, IntervalVar}, + targets={Objective, Constraint, LogicalConstraint, IntervalVar, + SequenceVar}, ) if unknown: raise ValueError( @@ -1386,6 +1388,10 @@ def solve(self, model, **kwds): ) else: sol = sol.get_value() + if py_var.ctype is SequenceVar: + # They don't actually have values--the IntervalVars will get + # set. + continue if py_var.ctype is IntervalVar: if len(sol) == 0: # The interval_var is absent diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index b563052ef3a..a3326b19cf4 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -12,7 +12,10 @@ import pyomo.common.unittest as unittest from pyomo.common.fileutils import Executable -from pyomo.contrib.cp import IntervalVar, Pulse, Step, AlwaysIn +from pyomo.contrib.cp import ( + IntervalVar, SequenceVar, Pulse, Step, AlwaysIn, + first_in_sequence, predecessor_to, no_overlap +) from pyomo.contrib.cp.repn.docplex_writer import LogicalToDoCplex from pyomo.environ import ( all_different, @@ -360,7 +363,6 @@ def test_matching_problem(self): results.solver.termination_condition, TerminationCondition.optimal ) self.assertEqual(value(m.obj), perfect) - m.person_name.pprint() self.assertEqual(value(m.person_name['P1']), 0) self.assertEqual(value(m.person_name['P2']), 1) self.assertEqual(value(m.person_name['P3']), 2) @@ -392,3 +394,24 @@ def test_matching_problem(self): results.solver.termination_condition, TerminationCondition.optimal ) self.assertEqual(value(m.obj), perfect) + + def test_scheduling_with_sequence_vars(self): + m = ConcreteModel() + m.Steps = Set(initialize=[1, 2, 3]) + def length_rule(m, j): + return 2*j + m.i = IntervalVar(m.Steps, start=(0, 12), end=(0, 12), length=length_rule) + m.seq = SequenceVar(expr=[m.i[j] for j in m.Steps]) + m.first = LogicalConstraint(expr=first_in_sequence(m.i[1], m.seq)) + m.seq_order1 = LogicalConstraint(expr=predecessor_to(m.i[1], m.i[2], m.seq)) + m.seq_order2 = LogicalConstraint(expr=predecessor_to(m.i[2], m.i[3], m.seq)) + m.no_ovlerpa = LogicalConstraint(expr=no_overlap(m.seq)) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertEqual(value(m.i[1].start_time), 0) + self.assertEqual(value(m.i[2].start_time), 2) + self.assertEqual(value(m.i[3].start_time), 6) + From ff23a4d7db83adbf78029095522a4bbb0c3f0a07 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 16:04:30 -0700 Subject: [PATCH 0168/1178] NFC: black --- pyomo/contrib/cp/repn/docplex_writer.py | 9 +++++++-- pyomo/contrib/cp/tests/test_docplex_writer.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index b1b8708c757..27e2f7ef8b9 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1158,8 +1158,13 @@ def write(self, model, **options): RangeSet, Port, }, - targets={Objective, Constraint, LogicalConstraint, IntervalVar, - SequenceVar}, + targets={ + Objective, + Constraint, + LogicalConstraint, + IntervalVar, + SequenceVar, + }, ) if unknown: raise ValueError( diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index a3326b19cf4..20511a8aa3d 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -13,8 +13,14 @@ from pyomo.common.fileutils import Executable from pyomo.contrib.cp import ( - IntervalVar, SequenceVar, Pulse, Step, AlwaysIn, - first_in_sequence, predecessor_to, no_overlap + IntervalVar, + SequenceVar, + Pulse, + Step, + AlwaysIn, + first_in_sequence, + predecessor_to, + no_overlap, ) from pyomo.contrib.cp.repn.docplex_writer import LogicalToDoCplex from pyomo.environ import ( @@ -398,8 +404,10 @@ def test_matching_problem(self): def test_scheduling_with_sequence_vars(self): m = ConcreteModel() m.Steps = Set(initialize=[1, 2, 3]) + def length_rule(m, j): - return 2*j + return 2 * j + m.i = IntervalVar(m.Steps, start=(0, 12), end=(0, 12), length=length_rule) m.seq = SequenceVar(expr=[m.i[j] for j in m.Steps]) m.first = LogicalConstraint(expr=first_in_sequence(m.i[1], m.seq)) @@ -414,4 +422,3 @@ def length_rule(m, j): self.assertEqual(value(m.i[1].start_time), 0) self.assertEqual(value(m.i[2].start_time), 2) self.assertEqual(value(m.i[3].start_time), 6) - From 14b1c5defa98b55f34425aacff48bf82f82b03a8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 11:32:44 -0700 Subject: [PATCH 0169/1178] Fixing a bug with printing precedence expressions with Param-valued delays. --- .../cp/scheduling_expr/precedence_expressions.py | 10 +++++----- .../cp/tests/test_precedence_constraints.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py index 5340583a216..1b7693605c9 100644 --- a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py @@ -21,13 +21,13 @@ def delay(self): return self._args_[2] def _to_string_impl(self, values, relation): - delay = int(values[2]) - if delay == 0: + delay = values[2] + if delay == '0': first = values[0] - elif delay > 0: - first = "%s + %s" % (values[0], delay) + elif delay[0] in '-+': + first = "%s %s %s" % (values[0], delay[0], delay[1:]) else: - first = "%s - %s" % (values[0], abs(delay)) + first = "%s + %s" % (values[0], delay) return "%s %s %s" % (first, relation, values[1]) diff --git a/pyomo/contrib/cp/tests/test_precedence_constraints.py b/pyomo/contrib/cp/tests/test_precedence_constraints.py index 461dabf564c..471b5bca512 100644 --- a/pyomo/contrib/cp/tests/test_precedence_constraints.py +++ b/pyomo/contrib/cp/tests/test_precedence_constraints.py @@ -15,7 +15,7 @@ BeforeExpression, AtExpression, ) -from pyomo.environ import ConcreteModel, LogicalConstraint +from pyomo.environ import ConcreteModel, LogicalConstraint, Param class TestPrecedenceRelationships(unittest.TestCase): @@ -173,3 +173,17 @@ def test_end_after_end(self): self.assertEqual(m.c.expr.delay, 0) self.assertEqual(str(m.c.expr), "b.end_time <= a.end_time") + + def test_end_before_start_param_delay(self): + m = self.get_model() + m.PrepTime = Param(initialize=5) + m.c = LogicalConstraint(expr=m.a.end_time.before(m.b.start_time, + delay=m.PrepTime)) + self.assertIsInstance(m.c.expr, BeforeExpression) + self.assertEqual(len(m.c.expr.args), 3) + self.assertIs(m.c.expr.args[0], m.a.end_time) + self.assertIs(m.c.expr.args[1], m.b.start_time) + self.assertIs(m.c.expr.delay, m.PrepTime) + + self.assertEqual(str(m.c.expr), "a.end_time + PrepTime <= b.start_time") + From 171f2db4fd703b888dd447abbe60a25d7fba0774 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 13:08:11 -0700 Subject: [PATCH 0170/1178] Fixing a bug with single-step-function cumulative functions in alwaysin --- pyomo/contrib/cp/repn/docplex_writer.py | 14 ++++++----- pyomo/contrib/cp/tests/test_docplex_walker.py | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 27e2f7ef8b9..2bfc96faa7d 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -621,22 +621,22 @@ def _before_interval_var_presence(visitor, child): def _handle_step_at_node(visitor, node): - return cp.step_at(node._time, node._height) + return False, (_GENERAL, cp.step_at(node._time, node._height)) def _handle_step_at_start_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._time) - return cp.step_at_start(cpx_var, node._height) + return False, (_GENERAL, cp.step_at_start(cpx_var, node._height)) def _handle_step_at_end_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._time) - return cp.step_at_end(cpx_var, node._height) + return False, (_GENERAL, cp.step_at_end(cpx_var, node._height)) def _handle_pulse_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._interval_var) - return cp.pulse(cpx_var, node._height) + return False, (_GENERAL, cp.pulse(cpx_var, node._height)) def _handle_negated_step_function_node(visitor, node): @@ -647,9 +647,9 @@ def _handle_cumulative_function(visitor, node): expr = 0 for arg in node.args: if arg.__class__ is NegatedStepFunction: - expr -= _handle_negated_step_function_node(visitor, arg) + expr -= _handle_negated_step_function_node(visitor, arg)[1][1] else: - expr += _step_function_handles[arg.__class__](visitor, arg) + expr += _step_function_handles[arg.__class__](visitor, arg)[1][1] return False, (_GENERAL, expr) @@ -1223,6 +1223,8 @@ def write(self, model, **options): # Write logical constraints for cons in components[LogicalConstraint]: + print(cons) + print(cons.expr) expr = visitor.walk_expression((cons.expr, cons, 0)) if expr[0] is _ELEMENT_CONSTRAINT: # Make the expression into a docplex-approved boolean-valued diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 560142ff410..dcc86033cc1 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1358,6 +1358,29 @@ def test_always_in(self): ) ) + def test_always_in_single_pulse(self): + # This is a bit silly as you can tell whether or not it is feasible + # structurally, but there's not reason it couldn't happen. + m = self.get_model() + f = Pulse((m.i, 3)) + m.c = LogicalConstraint(expr=f.within((0, 3), (0, 10))) + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.expr, m.c, 0)) + + self.assertIn(id(m.i), visitor.var_map) + + i = visitor.var_map[id(m.i)] + + self.assertTrue( + expr[1].equals( + cp.always_in( + cp.pulse(i, 3), + interval=(0, 10), + min=0, + max=3, + ) + ) + ) @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_NamedExpressions(CommonTest): From a75c9c6db7dbf9ccacd51c6e3058e0fce2b50310 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 13:11:11 -0700 Subject: [PATCH 0171/1178] Removing debugging --- pyomo/contrib/cp/repn/docplex_writer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 2bfc96faa7d..de71e4e98dd 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1223,8 +1223,6 @@ def write(self, model, **options): # Write logical constraints for cons in components[LogicalConstraint]: - print(cons) - print(cons.expr) expr = visitor.walk_expression((cons.expr, cons, 0)) if expr[0] is _ELEMENT_CONSTRAINT: # Make the expression into a docplex-approved boolean-valued From b5db4422bf4959e1628ec98817dd30b377ec171d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 15:19:27 -0700 Subject: [PATCH 0172/1178] Removing a *very* old (Pyomo 4.0) deprecation message that IntervalVars hit for convoluted reasons--but basically because they have no kwd args named 'rule' --- pyomo/core/base/block.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index fd5322ba686..ba23a6af654 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1112,26 +1112,13 @@ def add_component(self, name, val): # Error, for disabled support implicit rule names # if '_rule' in val.__dict__ and val._rule is None: - _found = False try: _test = val.local_name + '_rule' for i in (1, 2): frame = sys._getframe(i) - _found |= _test in frame.f_locals except: pass - if _found: - # JDS: Do not blindly reformat this message. The - # formatter inserts arbitrarily-long names(), which can - # cause the resulting logged message to be very poorly - # formatted due to long lines. - logger.warning( - """As of Pyomo 4.0, Pyomo components no longer support implicit rules. -You defined a component (%s) that appears -to rely on an implicit rule (%s). -Components must now specify their rules explicitly using 'rule=' keywords.""" - % (val.name, _test) - ) + # # Don't reconstruct if this component has already been constructed. # This allows a user to move a component from one block to From f9e62781d6a5afb4edeb75b0a9191f843eeb0fc8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 15:22:17 -0700 Subject: [PATCH 0173/1178] NFC: Would you believe that black doesn't approve --- pyomo/contrib/cp/tests/test_docplex_walker.py | 10 ++-------- pyomo/contrib/cp/tests/test_precedence_constraints.py | 6 +++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index dcc86033cc1..0b2057217c0 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1372,16 +1372,10 @@ def test_always_in_single_pulse(self): i = visitor.var_map[id(m.i)] self.assertTrue( - expr[1].equals( - cp.always_in( - cp.pulse(i, 3), - interval=(0, 10), - min=0, - max=3, - ) - ) + expr[1].equals(cp.always_in(cp.pulse(i, 3), interval=(0, 10), min=0, max=3)) ) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_NamedExpressions(CommonTest): def test_named_expression(self): diff --git a/pyomo/contrib/cp/tests/test_precedence_constraints.py b/pyomo/contrib/cp/tests/test_precedence_constraints.py index 471b5bca512..b4b9b8fee40 100644 --- a/pyomo/contrib/cp/tests/test_precedence_constraints.py +++ b/pyomo/contrib/cp/tests/test_precedence_constraints.py @@ -177,8 +177,9 @@ def test_end_after_end(self): def test_end_before_start_param_delay(self): m = self.get_model() m.PrepTime = Param(initialize=5) - m.c = LogicalConstraint(expr=m.a.end_time.before(m.b.start_time, - delay=m.PrepTime)) + m.c = LogicalConstraint( + expr=m.a.end_time.before(m.b.start_time, delay=m.PrepTime) + ) self.assertIsInstance(m.c.expr, BeforeExpression) self.assertEqual(len(m.c.expr.args), 3) self.assertIs(m.c.expr.args[0], m.a.end_time) @@ -186,4 +187,3 @@ def test_end_before_start_param_delay(self): self.assertIs(m.c.expr.delay, m.PrepTime) self.assertEqual(str(m.c.expr), "a.end_time + PrepTime <= b.start_time") - From 7187fc291dfb59373c4e73f4bf7bff880d9373ed Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 4 Jan 2024 15:50:21 -0700 Subject: [PATCH 0174/1178] Backwards compability: Process legacy options. --- pyomo/solver/base.py | 8 ++++---- pyomo/solver/ipopt.py | 23 +++++++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 48adc44c4d7..48b48db14b9 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -532,8 +532,8 @@ def options(self): class. """ for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - return getattr(self, solver_name + '_options') + if hasattr(self, 'solver_options'): + return getattr(self, 'solver_options') raise NotImplementedError('Could not find the correct options') @options.setter @@ -543,8 +543,8 @@ def options(self, val): """ found = False for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - setattr(self, solver_name + '_options', val) + if hasattr(self, 'solver_options'): + setattr(self, 'solver_options', val) found = True if not found: raise NotImplementedError('Could not find the correct options') diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 1b4c0eb36cb..406f4291c44 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -14,7 +14,7 @@ import datetime import io import sys -from typing import Mapping, Optional +from typing import Mapping, Optional, Dict from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat @@ -188,7 +188,7 @@ def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() self._writer.config.skip_trivial_constraints = True - self.ipopt_options = self._config.solver_options + self._solver_options = self._config.solver_options def available(self): if self.config.executable.path() is None: @@ -216,6 +216,14 @@ def config(self): def config(self, val): self._config = val + @property + def solver_options(self): + return self._solver_options + + @solver_options.setter + def solver_options(self, val: Dict): + self._solver_options = val + @property def symbol_map(self): return self._symbol_map @@ -240,15 +248,14 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd = [str(config.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') - if 'option_file_name' in config.solver_options: + if 'option_file_name' in self.solver_options: raise ValueError( 'Pyomo generates the ipopt options file as part of the solve method. ' 'Add all options to ipopt.config.solver_options instead.' ) - self.ipopt_options = dict(config.solver_options) - if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: - self.ipopt_options['max_cpu_time'] = config.time_limit - for k, val in self.ipopt_options.items(): + if config.time_limit is not None and 'max_cpu_time' not in self.solver_options: + self.solver_options['max_cpu_time'] = config.time_limit + for k, val in self.solver_options.items(): if k in ipopt_command_line_options: cmd.append(str(k) + '=' + str(val)) return cmd @@ -309,7 +316,7 @@ def solve(self, model, **kwds): # Write the opt_file, if there should be one; return a bool to say # whether or not we have one (so we can correctly build the command line) opt_file = self._write_options_file( - filename=basename, options=config.solver_options + filename=basename, options=self.solver_options ) # Call ipopt - passing the files via the subprocess cmd = self._create_command_line( From 6fb37ce225905bcd5d59924c6a404142f114b30b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 4 Jan 2024 16:04:27 -0700 Subject: [PATCH 0175/1178] Add new option to legacy interface for forwards compability --- pyomo/solver/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 48b48db14b9..9b52c61f642 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -387,6 +387,7 @@ def solve( options: Optional[Dict] = None, keepfiles: bool = False, symbolic_solver_labels: bool = False, + raise_exception_on_nonoptimal_result: bool = False ): """ Solve method: maps new solve method style to backwards compatible version. @@ -404,6 +405,9 @@ def solve( self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit self.config.report_timing = report_timing + # This is a new flag in the interface. To preserve backwards compability, + # its default is set to "False" + self.config.raise_exception_on_nonoptimal_result = raise_exception_on_nonoptimal_result if solver_io is not None: raise NotImplementedError('Still working on this') if suffixes is not None: From a79f34856be419de027f1f59abed357850bfb758 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 4 Jan 2024 16:09:03 -0700 Subject: [PATCH 0176/1178] Apply black --- pyomo/solver/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 9b52c61f642..202b0422cee 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -387,7 +387,7 @@ def solve( options: Optional[Dict] = None, keepfiles: bool = False, symbolic_solver_labels: bool = False, - raise_exception_on_nonoptimal_result: bool = False + raise_exception_on_nonoptimal_result: bool = False, ): """ Solve method: maps new solve method style to backwards compatible version. @@ -407,7 +407,9 @@ def solve( self.config.report_timing = report_timing # This is a new flag in the interface. To preserve backwards compability, # its default is set to "False" - self.config.raise_exception_on_nonoptimal_result = raise_exception_on_nonoptimal_result + self.config.raise_exception_on_nonoptimal_result = ( + raise_exception_on_nonoptimal_result + ) if solver_io is not None: raise NotImplementedError('Still working on this') if suffixes is not None: From e889912eea041cc4f07337588151fb219d0d2d16 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 9 Jan 2024 12:19:41 -0700 Subject: [PATCH 0177/1178] Added interface to ensure parmest backwards compatiblity. --- .../parmest/examples_deprecated/__init__.py | 10 + .../reaction_kinetics/__init__.py | 10 + .../simple_reaction_parmest_example.py | 118 ++ .../reactor_design/__init__.py | 10 + .../reactor_design/bootstrap_example.py | 60 + .../reactor_design/datarec_example.py | 100 ++ .../reactor_design/leaveNout_example.py | 98 ++ .../likelihood_ratio_example.py | 64 + .../multisensor_data_example.py | 51 + .../parameter_estimation_example.py | 58 + .../reactor_design/reactor_data.csv | 20 + .../reactor_data_multisensor.csv | 20 + .../reactor_data_timeseries.csv | 20 + .../reactor_design/reactor_design.py | 104 ++ .../reactor_design/timeseries_data_example.py | 55 + .../rooney_biegler/__init__.py | 10 + .../rooney_biegler/bootstrap_example.py | 57 + .../likelihood_ratio_example.py | 62 + .../parameter_estimation_example.py | 60 + .../rooney_biegler/rooney_biegler.py | 60 + .../rooney_biegler_with_constraint.py | 63 + .../examples_deprecated/semibatch/__init__.py | 10 + .../semibatch/bootstrap_theta.csv | 101 ++ .../semibatch/obj_at_theta.csv | 1009 ++++++++++++ .../semibatch/parallel_example.py | 57 + .../semibatch/parameter_estimation_example.py | 42 + .../semibatch/scenario_example.py | 52 + .../semibatch/scenarios.csv | 11 + .../semibatch/semibatch.py | 287 ++++ pyomo/contrib/parmest/parmest.py | 101 +- pyomo/contrib/parmest/parmest_deprecated.py | 1366 +++++++++++++++++ pyomo/contrib/parmest/scenariocreator.py | 28 +- .../parmest/scenariocreator_deprecated.py | 166 ++ 33 files changed, 4328 insertions(+), 12 deletions(-) create mode 100644 pyomo/contrib/parmest/examples_deprecated/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py create mode 100644 pyomo/contrib/parmest/parmest_deprecated.py create mode 100644 pyomo/contrib/parmest/scenariocreator_deprecated.py diff --git a/pyomo/contrib/parmest/examples_deprecated/__init__.py b/pyomo/contrib/parmest/examples_deprecated/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py new file mode 100644 index 00000000000..719a930251c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py @@ -0,0 +1,118 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +''' +Example from Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) + +This example shows: +1. How to define the unknown (to be regressed parameters) with an index +2. How to call parmest to only estimate some of the parameters (and fix the rest) + +Code provided by Paul Akula. +''' + +from pyomo.environ import ( + ConcreteModel, + Param, + Var, + PositiveReals, + Objective, + Constraint, + RangeSet, + Expression, + minimize, + exp, + value, +) +import pyomo.contrib.parmest.parmest as parmest + + +def simple_reaction_model(data): + # Create the concrete model + model = ConcreteModel() + + model.x1 = Param(initialize=float(data['x1'])) + model.x2 = Param(initialize=float(data['x2'])) + + # Rate constants + model.rxn = RangeSet(2) + initial_guess = {1: 750, 2: 1200} + model.k = Var(model.rxn, initialize=initial_guess, within=PositiveReals) + + # reaction product + model.y = Expression(expr=exp(-model.k[1] * model.x1 * exp(-model.k[2] / model.x2))) + + # fix all of the regressed parameters + model.k.fix() + + # =================================================================== + # Stage-specific cost computations + def ComputeFirstStageCost_rule(model): + return 0 + + model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return (float(data['y']) - m.y) ** 2 + + model.SecondStageCost = Expression(rule=AllMeasurements) + + def total_cost_rule(m): + return m.FirstStageCost + m.SecondStageCost + + model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + return model + + +def main(): + # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) + data = [ + {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, + {'experiment': 2, 'x1': 0.2, 'x2': 100, 'y': 0.983}, + {'experiment': 3, 'x1': 0.3, 'x2': 100, 'y': 0.955}, + {'experiment': 4, 'x1': 0.4, 'x2': 100, 'y': 0.979}, + {'experiment': 5, 'x1': 0.5, 'x2': 100, 'y': 0.993}, + {'experiment': 6, 'x1': 0.05, 'x2': 200, 'y': 0.626}, + {'experiment': 7, 'x1': 0.1, 'x2': 200, 'y': 0.544}, + {'experiment': 8, 'x1': 0.15, 'x2': 200, 'y': 0.455}, + {'experiment': 9, 'x1': 0.2, 'x2': 200, 'y': 0.225}, + {'experiment': 10, 'x1': 0.25, 'x2': 200, 'y': 0.167}, + {'experiment': 11, 'x1': 0.02, 'x2': 300, 'y': 0.566}, + {'experiment': 12, 'x1': 0.04, 'x2': 300, 'y': 0.317}, + {'experiment': 13, 'x1': 0.06, 'x2': 300, 'y': 0.034}, + {'experiment': 14, 'x1': 0.08, 'x2': 300, 'y': 0.016}, + {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, + ] + + # ======================================================================= + # Parameter estimation without covariance estimate + # Only estimate the parameter k[1]. The parameter k[2] will remain fixed + # at its initial value + theta_names = ['k[1]'] + pest = parmest.Estimator(simple_reaction_model, data, theta_names) + obj, theta = pest.theta_est() + print(obj) + print(theta) + print() + + # ======================================================================= + # Estimate both k1 and k2 and compute the covariance matrix + theta_names = ['k'] + pest = parmest.Estimator(simple_reaction_model, data, theta_names) + n = 15 # total number of data points used in the objective (y in 15 scenarios) + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) + print(obj) + print(theta) + print(cov) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py new file mode 100644 index 00000000000..e2d172f34f6 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py @@ -0,0 +1,60 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Parameter estimation with bootstrap resampling + bootstrap_theta = pest.theta_est_bootstrap(50) + + # Plot results + parmest.graphics.pairwise_plot(bootstrap_theta, title="Bootstrap theta") + parmest.graphics.pairwise_plot( + bootstrap_theta, + theta, + 0.8, + ["MVN", "KDE", "Rect"], + title="Bootstrap theta with confidence regions", + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py new file mode 100644 index 00000000000..cfd3891c00e --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py @@ -0,0 +1,100 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + +np.random.seed(1234) + + +def reactor_design_model_for_datarec(data): + # Unfix inlet concentration for data rec + model = reactor_design_model(data) + model.caf.fixed = False + + return model + + +def generate_data(): + ### Generate data based on real sv, caf, ca, cb, cc, and cd + sv_real = 1.05 + caf_real = 10000 + ca_real = 3458.4 + cb_real = 1060.8 + cc_real = 1683.9 + cd_real = 1898.5 + + data = pd.DataFrame() + ndata = 200 + # Normal distribution, mean = 3400, std = 500 + data["ca"] = 500 * np.random.randn(ndata) + 3400 + # Random distribution between 500 and 1500 + data["cb"] = np.random.rand(ndata) * 1000 + 500 + # Lognormal distribution + data["cc"] = np.random.lognormal(np.log(1600), 0.25, ndata) + # Triangular distribution between 1000 and 2000 + data["cd"] = np.random.triangular(1000, 1800, 3000, size=ndata) + + data["sv"] = sv_real + data["caf"] = caf_real + + return data + + +def main(): + # Generate data + data = generate_data() + data_std = data.std() + + # Define sum of squared error objective function for data rec + def SSE(model, data): + expr = ( + ((float(data.iloc[0]["ca"]) - model.ca) / float(data_std["ca"])) ** 2 + + ((float(data.iloc[0]["cb"]) - model.cb) / float(data_std["cb"])) ** 2 + + ((float(data.iloc[0]["cc"]) - model.cc) / float(data_std["cc"])) ** 2 + + ((float(data.iloc[0]["cd"]) - model.cd) / float(data_std["cd"])) ** 2 + ) + return expr + + ### Data reconciliation + theta_names = [] # no variables to estimate, use initialized values + + pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) + + obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) + print(obj) + print(theta) + + parmest.graphics.grouped_boxplot( + data[["ca", "cb", "cc", "cd"]], + data_rec[["ca", "cb", "cc", "cd"]], + group_names=["Data", "Data Rec"], + ) + + ### Parameter estimation using reconciled data + theta_names = ["k1", "k2", "k3"] + data_rec["sv"] = data["sv"] + + pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) + obj, theta = pest.theta_est() + print(obj) + print(theta) + + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} + print(theta_real) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py new file mode 100644 index 00000000000..6952a7fc733 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py @@ -0,0 +1,98 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Create more data for the example + N = 50 + df_std = data.std().to_frame().transpose() + df_rand = pd.DataFrame(np.random.normal(size=N)) + df_sample = data.sample(N, replace=True).reset_index(drop=True) + data = df_sample + df_rand.dot(df_std) / 10 + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + print(obj) + print(theta) + + ### Parameter estimation with 'leave-N-out' + # Example use case: For each combination of data where one data point is left + # out, estimate theta + lNo_theta = pest.theta_est_leaveNout(1) + print(lNo_theta.head()) + + parmest.graphics.pairwise_plot(lNo_theta, theta) + + ### Leave one out/boostrap analysis + # Example use case: leave 25 data points out, run 20 bootstrap samples with the + # remaining points, determine if the theta estimate using the points left out + # is inside or outside an alpha region based on the bootstrap samples, repeat + # 5 times. Results are stored as a list of tuples, see API docs for information. + lNo = 25 + lNo_samples = 5 + bootstrap_samples = 20 + dist = "MVN" + alphas = [0.7, 0.8, 0.9] + + results = pest.leaveNout_bootstrap_test( + lNo, lNo_samples, bootstrap_samples, dist, alphas, seed=524 + ) + + # Plot results for a single value of alpha + alpha = 0.8 + for i in range(lNo_samples): + theta_est_N = results[i][1] + bootstrap_results = results[i][2] + parmest.graphics.pairwise_plot( + bootstrap_results, + theta_est_N, + alpha, + ["MVN"], + title="Alpha: " + str(alpha) + ", " + str(theta_est_N.loc[0, alpha]), + ) + + # Extract the percent of points that are within the alpha region + r = [results[i][1].loc[0, alpha] for i in range(lNo_samples)] + percent_true = sum(r) / len(r) + print(percent_true) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py new file mode 100644 index 00000000000..a0fe6f22305 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py @@ -0,0 +1,64 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +from itertools import product +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Find the objective value at each theta estimate + k1 = [0.8, 0.85, 0.9] + k2 = [1.6, 1.65, 1.7] + k3 = [0.00016, 0.000165, 0.00017] + theta_vals = pd.DataFrame(list(product(k1, k2, k3)), columns=["k1", "k2", "k3"]) + obj_at_theta = pest.objective_at_theta(theta_vals) + + # Run the likelihood ratio test + LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) + + # Plot results + parmest.graphics.pairwise_plot( + LR, theta, 0.9, title="LR results within 90% confidence region" + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py new file mode 100644 index 00000000000..a92ac626fae --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Parameter estimation using multisensor data + + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data, includes multiple sensors for ca and cc + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE_multisensor(model, data): + expr = ( + ((float(data.iloc[0]["ca1"]) - model.ca) ** 2) * (1 / 3) + + ((float(data.iloc[0]["ca2"]) - model.ca) ** 2) * (1 / 3) + + ((float(data.iloc[0]["ca3"]) - model.ca) ** 2) * (1 / 3) + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + ((float(data.iloc[0]["cc1"]) - model.cc) ** 2) * (1 / 2) + + ((float(data.iloc[0]["cc2"]) - model.cc) ** 2) * (1 / 2) + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) + obj, theta = pest.theta_est() + print(obj) + print(theta) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py new file mode 100644 index 00000000000..581d3904c04 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py @@ -0,0 +1,58 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Assert statements compare parameter estimation (theta) to an expected value + k1_expected = 5.0 / 6.0 + k2_expected = 5.0 / 3.0 + k3_expected = 1.0 / 6000.0 + relative_error = abs(theta["k1"] - k1_expected) / k1_expected + assert relative_error < 0.05 + relative_error = abs(theta["k2"] - k2_expected) / k2_expected + assert relative_error < 0.05 + relative_error = abs(theta["k3"] - k3_expected) / k3_expected + assert relative_error < 0.05 + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv new file mode 100644 index 00000000000..c0695c049c4 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv @@ -0,0 +1,20 @@ +sv,caf,ca,cb,cc,cd +1.06,10010,3407.4,945.4,1717.1,1931.9 +1.11,10010,3631.6,1247.2,1694.1,1960.6 +1.16,10010,3645.3,971.4,1552.3,1898.8 +1.21,10002,3536.2,1225.9,1351.1,1757.0 +1.26,10002,3755.6,1263.8,1562.3,1952.2 +1.30,10007,3598.3,1153.4,1413.4,1903.3 +1.35,10007,3939.0,971.4,1416.9,1794.9 +1.41,10009,4227.9,986.3,1188.7,1821.5 +1.45,10001,4163.1,972.5,1085.6,1908.7 +1.50,10002,3896.3,977.3,1132.9,2080.5 +1.56,10004,3801.6,1040.6,1157.7,1780.0 +1.60,10008,4128.4,1198.6,1150.0,1581.9 +1.66,10002,4385.4,1158.7,970.0,1629.8 +1.70,10007,3960.8,1194.9,1091.2,1835.5 +1.76,10007,4180.8,1244.2,1034.8,1739.5 +1.80,10001,4212.3,1240.7,1010.3,1739.6 +1.85,10004,4200.2,1164.0,931.5,1783.7 +1.90,10009,4748.6,1037.9,1065.9,1685.6 +1.96,10009,4941.3,1038.5,996.0,1855.7 diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv new file mode 100644 index 00000000000..9df745a8422 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv @@ -0,0 +1,20 @@ +sv,caf,ca1,ca2,ca3,cb,cc1,cc2,cd +1.06,10010,3407.4,3363.1,3759.1,945.4,1717.1,1695.1,1931.9 +1.11,10010,3631.6,3345.2,3906.0,1247.2,1694.1,1536.7,1960.6 +1.16,10010,3645.3,3784.9,3301.3,971.4,1552.3,1496.2,1898.8 +1.21,10002,3536.2,3718.3,3678.5,1225.9,1351.1,1549.7,1757.0 +1.26,10002,3755.6,3731.8,3854.7,1263.8,1562.3,1410.1,1952.2 +1.30,10007,3598.3,3751.6,3722.5,1153.4,1413.4,1291.6,1903.3 +1.35,10007,3939.0,3969.5,3827.2,971.4,1416.9,1276.8,1794.9 +1.41,10009,4227.9,3721.3,4046.7,986.3,1188.7,1221.0,1821.5 +1.45,10001,4163.1,4142.7,4512.1,972.5,1085.6,1212.1,1908.7 +1.50,10002,3896.3,3953.7,4028.0,977.3,1132.9,1167.7,2080.5 +1.56,10004,3801.6,4263.3,4015.3,1040.6,1157.7,1236.5,1780.0 +1.60,10008,4128.4,4061.1,3914.8,1198.6,1150.0,1032.2,1581.9 +1.66,10002,4385.4,4344.7,4006.8,1158.7,970.0,1155.1,1629.8 +1.70,10007,3960.8,4259.1,4274.7,1194.9,1091.2,958.6,1835.5 +1.76,10007,4180.8,4071.1,4598.7,1244.2,1034.8,1086.8,1739.5 +1.80,10001,4212.3,4541.8,4440.0,1240.7,1010.3,920.8,1739.6 +1.85,10004,4200.2,4444.9,4667.2,1164.0,931.5,850.7,1783.7 +1.90,10009,4748.6,4813.4,4753.2,1037.9,1065.9,898.5,1685.6 +1.96,10009,4941.3,4511.8,4405.4,1038.5,996.0,921.9,1855.7 diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv new file mode 100644 index 00000000000..1421cfef6a0 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv @@ -0,0 +1,20 @@ +experiment,time,sv,caf,ca,cb,cc,cd +0,18000,1.075,10008,3537.5,1077.2,1591.2,1938.7 +0,18060,1.121,10002,3547.7,1186.2,1766.3,1946.9 +0,18120,1.095,10005,3614.4,1009.9,1702.9,1841.8 +0,18180,1.102,10007,3443.7,863.1,1666.2,1918.7 +0,18240,1.105,10002,3687.1,1052.1,1501.7,1905.0 +0,18300,1.084,10008,3452.7,1000.5,1512.0,2043.4 +1,18360,1.159,10009,3427.8,1133.1,1481.1,1837.1 +1,18420,1.432,10010,4029.8,1058.8,1213.0,1911.1 +1,18480,1.413,10005,3953.1,960.1,1304.8,1754.3 +1,18540,1.475,10008,4034.8,1121.2,1351.0,1992.0 +1,18600,1.433,10002,4029.8,1100.6,1199.5,1713.9 +1,18660,1.488,10006,3972.8,1148.0,1380.7,1992.1 +1,18720,1.456,10003,4031.2,1145.2,1133.1,1812.6 +2,18780,1.821,10008,4499.1,980.8,924.7,1840.9 +2,18840,1.856,10005,4370.9,1000.7,833.4,1848.4 +2,18900,1.846,10002,4438.6,1038.6,1042.8,1703.3 +2,18960,1.852,10002,4468.4,1151.8,1119.1,1564.8 +2,19020,1.865,10009,4341.6,1060.5,844.2,1974.8 +2,19080,1.872,10002,4427.0,964.6,840.2,1928.5 diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py new file mode 100644 index 00000000000..16f65e236eb --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py @@ -0,0 +1,104 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +Continuously stirred tank reactor model, based on +pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py +""" +import pandas as pd +from pyomo.environ import ( + ConcreteModel, + Param, + Var, + PositiveReals, + Objective, + Constraint, + maximize, + SolverFactory, +) + + +def reactor_design_model(data): + # Create the concrete model + model = ConcreteModel() + + # Rate constants + model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 + model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 + model.k3 = Param( + initialize=1.0 / 6000.0, within=PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = Param(initialize=float(data["caf"]), within=PositiveReals) + elif isinstance(data, pd.DataFrame): + model.caf = Param(initialize=float(data.iloc[0]["caf"]), within=PositiveReals) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = Param(initialize=float(data["sv"]), within=PositiveReals) + elif isinstance(data, pd.DataFrame): + model.sv = Param(initialize=float(data.iloc[0]["sv"]), within=PositiveReals) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = Var(initialize=5000.0, within=PositiveReals) + model.cb = Var(initialize=2000.0, within=PositiveReals) + model.cc = Var(initialize=2000.0, within=PositiveReals) + model.cd = Var(initialize=1000.0, within=PositiveReals) + + # Objective + model.obj = Objective(expr=model.cb, sense=maximize) + + # Constraints + model.ca_bal = Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = Constraint( + expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) + ) + + model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + + model.cd_bal = Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + +def main(): + # For a range of sv values, return ca, cb, cc, and cd + results = [] + sv_values = [1.0 + v * 0.05 for v in range(1, 20)] + caf = 10000 + for sv in sv_values: + model = reactor_design_model(pd.DataFrame(data={"caf": [caf], "sv": [sv]})) + solver = SolverFactory("ipopt") + solver.solve(model) + results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) + + results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) + print(results) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py new file mode 100644 index 00000000000..da2ab1874c9 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py @@ -0,0 +1,55 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname + +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Parameter estimation using timeseries data + + # Vars to estimate + theta_names = ['k1', 'k2', 'k3'] + + # Data, includes multiple sensors for ca and cc + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) + data = pd.read_csv(file_name) + + # Group time series data into experiments, return the mean value for sv and caf + # Returns a list of dictionaries + data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + + def SSE_timeseries(model, data): + expr = 0 + for val in data['ca']: + expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) + for val in data['cb']: + expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) + for val in data['cc']: + expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) + for val in data['cd']: + expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) + return expr + + pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) + obj, theta = pest.theta_est() + print(obj) + print(theta) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py new file mode 100644 index 00000000000..f686bbd933d --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py @@ -0,0 +1,57 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, +) + + +def main(): + # Vars to estimate + theta_names = ['asymptote', 'rate_constant'] + + # Data + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Parameter estimation with bootstrap resampling + bootstrap_theta = pest.theta_est_bootstrap(50, seed=4581) + + # Plot results + parmest.graphics.pairwise_plot(bootstrap_theta, title='Bootstrap theta') + parmest.graphics.pairwise_plot( + bootstrap_theta, + theta, + 0.8, + ['MVN', 'KDE', 'Rect'], + title='Bootstrap theta with confidence regions', + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py new file mode 100644 index 00000000000..5e54a33abda --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py @@ -0,0 +1,62 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +from itertools import product +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, +) + + +def main(): + # Vars to estimate + theta_names = ['asymptote', 'rate_constant'] + + # Data + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Find the objective value at each theta estimate + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.1) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] + ) + obj_at_theta = pest.objective_at_theta(theta_vals) + + # Run the likelihood ratio test + LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) + + # Plot results + parmest.graphics.pairwise_plot( + LR, theta, 0.8, title='LR results within 80% confidence region' + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py new file mode 100644 index 00000000000..9af33217fe4 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py @@ -0,0 +1,60 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, +) + + +def main(): + # Vars to estimate + theta_names = ['asymptote', 'rate_constant'] + + # Data + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + + # Parameter estimation and covariance + n = 6 # total number of data points used in the objective (y in 6 scenarios) + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) + + # Plot theta estimates using a multivariate Gaussian distribution + parmest.graphics.pairwise_plot( + (theta, cov, 100), + theta_star=theta, + alpha=0.8, + distributions=['MVN'], + title='Theta estimates within 80% confidence region', + ) + + # Assert statements compare parameter estimation (theta) to an expected value + relative_error = abs(theta['asymptote'] - 19.1426) / 19.1426 + assert relative_error < 0.01 + relative_error = abs(theta['rate_constant'] - 0.5311) / 0.5311 + assert relative_error < 0.01 + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py new file mode 100644 index 00000000000..5a0e1238e85 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py @@ -0,0 +1,60 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for +model parameter uncertainty using nonlinear confidence regions. AIChE Journal, +47(8), 1794-1804. +""" + +import pandas as pd +import pyomo.environ as pyo + + +def rooney_biegler_model(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + +def main(): + # These were taken from Table A1.4 in Bates and Watts (1988). + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + model = rooney_biegler_model(data) + solver = pyo.SolverFactory('ipopt') + solver.solve(model) + + print('asymptote = ', model.asymptote()) + print('rate constant = ', model.rate_constant()) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py new file mode 100644 index 00000000000..2582e3fe928 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py @@ -0,0 +1,63 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for +model parameter uncertainty using nonlinear confidence regions. AIChE Journal, +47(8), 1794-1804. +""" + +import pandas as pd +import pyomo.environ as pyo + + +def rooney_biegler_model_with_constraint(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.response_function = pyo.Var(data.hour, initialize=0.0) + + # changed from expression to constraint + def response_rule(m, h): + return m.response_function[h] == m.asymptote * ( + 1 - pyo.exp(-m.rate_constant * h) + ) + + model.response_function_constraint = pyo.Constraint(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + +def main(): + # These were taken from Table A1.4 in Bates and Watts (1988). + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + model = rooney_biegler_model_with_constraint(data) + solver = pyo.SolverFactory('ipopt') + solver.solve(model) + + print('asymptote = ', model.asymptote()) + print('rate constant = ', model.rate_constant()) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv b/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv new file mode 100644 index 00000000000..29923a782c5 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv @@ -0,0 +1,101 @@ +,k1,k2,E1,E2 +0,23.8359813557911,149.99999125263844,31164.260824269295,41489.69422529956 +1,19.251987486659512,105.3374117880675,30505.86059307485,40516.897897740404 +2,19.31940450911214,105.78105886426505,30509.636888745794,40539.53548872927 +3,8.754357429283429,149.99988037658665,28334.500331107014,41482.01554893696 +4,23.016722464092286,80.03743984792878,31091.61503716734,39770.08415278276 +5,6.612337410520649,140.0259411600077,27521.46259880474,41302.159495413296 +6,14.29348509961158,147.0817016641302,29605.749859593245,41443.12009807534 +7,14.152069480386153,149.9914759675382,29676.633227079245,41483.41029455195 +8,19.081046896092914,125.55106106390114,30586.57857977985,41005.60243351924 +9,3.063566173952205,149.9999684548014,25473.079370305273,41483.426370389796 +10,17.79494440791066,108.52425726918327,30316.710830136202,40618.63715404914 +11,97.307579412204,149.99998972597675,35084.37589956093,41485.835559276136 +12,20.793042577945116,91.124365144131,30782.17494940993,40138.215713547994 +13,12.740540794730641,89.86327635412908,29396.65520336387,40086.14665722912 +14,6.930810780299319,149.99999327266906,27667.0240033497,41480.188987754496 +15,20.29404799567638,101.07539817765885,30697.087258737916,40443.316578889426 +16,85.77501788788223,149.99996482096984,34755.77375009206,41499.23448818336 +17,24.13150325243255,77.06876294766496,31222.03914354306,39658.418332258894 +18,16.026645517712712,149.99993094015056,30015.46332620076,41490.69892111652 +19,31.020018442708537,62.11558789585982,31971.311996398897,39089.828285017575 +20,20.815008037484656,87.35968459422139,30788.643843293,40007.78137819648 +21,19.007148519616447,96.44320176694993,30516.36933261116,40284.23312198372 +22,22.232021812057308,89.71692873746096,30956.252845068626,40095.009765519 +23,16.830765834427297,120.65209863104229,30139.92208332896,40912.673450399234 +24,15.274799190396566,129.82767733073857,29780.055282261117,41078.04749417758 +25,22.37343657709118,82.32861355430458,31013.57952062852,39853.06284595207 +26,9.055694749134819,149.99987339406314,28422.482259116612,41504.97564187301 +27,19.909770949417275,86.5634026379812,30705.60369894775,39996.134938503914 +28,20.604557306290886,87.96473948102359,30786.467003867263,40051.28176004557 +29,21.94101237923462,88.18216423767153,30942.372558158557,40051.20357069738 +30,3.200663718121338,149.99997712051055,25472.46099917771,41450.884180452646 +31,20.5812467558026,86.36098672832426,30802.74421085271,40010.76777825347 +32,18.776139793586893,108.99943042186453,30432.474809193136,40641.48011315501 +33,17.14246930769276,112.29370332257908,30164.332101438307,40684.867629869856 +34,20.52146255576043,99.7078140453859,30727.90573864389,40401.20730725967 +35,17.05073306185531,66.00385439687035,30257.075479935145,39247.26647870223 +36,7.1238843213074015,51.05163218895348,27811.250260416655,38521.11199236329 +37,10.54291332571747,76.74902426944477,28763.52244085013,39639.92644514267 +38,16.329028964122656,107.60037882134996,30073.5111433796,40592.825374177235 +39,18.0923131790489,107.75659679748213,30355.62290415686,40593.10521263782 +40,15.477264179087811,149.99995828085014,29948.62617372307,41490.770726165414 +41,23.190670255199933,76.5654091811839,31107.96477489951,39635.650879492074 +42,20.34720227734719,90.07051780196629,30716.131795936217,40096.932765428995 +43,23.60627359054596,80.0847207027996,31130.449736501876,39756.06693747353 +44,22.54968153535252,83.72995448206636,31038.51932262643,39906.60181934743 +45,24.951320839961582,67.97010976959977,31356.00147390564,39307.75709154711 +46,61.216667588824386,149.9999967830529,33730.22100500659,41474.80665231048 +47,9.797300324197744,136.33054557076974,28588.83540859912,41222.22413163186 +48,21.75078861615545,139.82641444329093,30894.847060525986,41290.16131583715 +49,21.76324066920255,99.57885291658233,30860.292260186063,40386.00605205238 +50,20.244262248110417,86.2553098058883,30742.054735645124,39981.83946305757 +51,21.859217291379004,72.89837327878459,30999.703939831277,39514.23768439393 +52,20.902111153308944,88.36862895882298,30782.76240691508,40033.44884393017 +53,59.58504995089654,149.9999677447201,33771.647879014425,41496.69202917452 +54,21.63994234351529,80.9641923004028,30933.578583737795,39809.523930207484 +55,9.804873383156298,149.9995892138235,28729.93818644509,41500.94496844104 +56,9.517359502437172,149.99308840029815,28505.329315103318,41470.65218792529 +57,19.923610217578116,88.23847592895486,30636.024864041487,40020.79650218989 +58,20.366495228182394,85.1991151089578,30752.560133063143,39947.719888972904 +59,12.242715793208157,149.99998097746882,29308.42752633667,41512.25071862387 +60,19.677765799324447,97.30674967097808,30618.37668428642,40323.0499230797 +61,19.03651315222424,109.20775378637025,30455.39615515442,40614.722801684395 +62,21.37660531151217,149.99999616215425,30806.121697474813,41479.3976433347 +63,21.896838392882998,86.86206456282005,30918.823491874144,39986.262281131254 +64,5.030122322262226,149.99991736085678,26792.302062236955,41480.579525893794 +65,17.851755694421776,53.33521102556455,30419.017295420916,38644.47349861614 +66,20.963796542255896,90.72302887846234,30795.751244616677,40114.19163802526 +67,23.082992539267945,77.24345020180209,31107.07485019312,39665.22410226011 +68,18.953050386839383,90.80802949182345,30529.280393040182,40113.73467038244 +69,20.710937910951355,83.16996057131982,30805.892332796295,39876.270184728084 +70,18.18549080794899,65.72657652078952,30416.294615296756,39223.21339606898 +71,12.147892028456324,45.12945045196771,29302.888575028635,38194.144730342545 +72,4.929663537166405,133.89086200105797,26635.8524254091,41163.82082194103 +73,20.512731504598662,106.98199797354127,30660.67479570742,40560.70063653076 +74,21.006700520199008,93.35471748418676,30761.272887418058,40178.10564855804 +75,19.73635577733317,98.75362910260881,30599.64039254174,40346.31274388047 +76,3.6393630101175565,149.99998305638113,25806.925407145678,41446.42489819377 +77,14.430958212981363,149.9999928114441,29710.277666486683,41478.96029884101 +78,21.138173237661093,90.73414659450283,30833.36092609432,40128.61898313504 +79,19.294823672883208,104.69324605284973,30510.371654343133,40510.84889949937 +80,2.607050470695225,69.22680095813037,25000.001468502505,39333.142090801295 +81,16.949842823156228,118.76691429120146,30074.04126731665,40824.66852388976 +82,21.029588811317897,95.27115352081795,30770.753828753943,40243.47156167542 +83,18.862418349044077,111.08370690591005,30421.17882623639,40670.941374189555 +84,24.708015660945147,76.24225941680999,31286.7038829574,39632.545034540664 +85,21.58937721477476,92.6329553952883,30871.989108388123,40181.7478528116 +86,21.091322126816706,96.07721666941696,30765.91144819689,40265.321194575095 +87,19.337815749868728,96.50567420686403,30604.551156564357,40318.12321325275 +88,17.77732130279279,108.5062535737451,30287.456682982094,40602.76307166587 +89,15.259532609396405,134.79914728383426,29793.69015375863,41199.11159557717 +90,21.616910309091583,90.65235108674251,30848.137718134392,40096.0776408459 +91,3.3372937891220475,149.99991062247588,25630.388452101062,41483.30064805118 +92,20.652437906744403,97.86062128528714,30747.864718937744,40330.11871286893 +93,22.134113060054425,73.68464943802763,31013.225174702933,39535.65213713519 +94,20.297310066178802,93.79207093658654,30684.309981457223,40191.747572763874 +95,6.007958386675472,149.99997175883215,27126.707007542,41465.75099589974 +96,16.572749402536758,40.75746000309888,30154.396795028595,37923.85448825053 +97,21.235697111801056,98.97798760165126,30807.097617165928,40373.550932032136 +98,20.10350615639414,96.19608053749371,30632.029399836003,40258.3813340696 +99,18.274272179970747,96.49060573948069,30456.872524151822,40305.258325587834 diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv new file mode 100644 index 00000000000..79f03e07dcd --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv @@ -0,0 +1,1009 @@ +,k1,k2,E1,E2,obj +0,4,40,29000,38000,667.4023645794207 +1,4,40,29000,38500,665.8312183437167 +2,4,40,29000,39000,672.7539769993407 +3,4,40,29000,39500,684.9503752463216 +4,4,40,29000,40000,699.985589093255 +5,4,40,29000,40500,716.1241770970677 +6,4,40,29000,41000,732.2023201586336 +7,4,40,29000,41500,747.4931745925483 +8,4,40,29500,38000,907.4405527163311 +9,4,40,29500,38500,904.2229271927299 +10,4,40,29500,39000,907.6942345285257 +11,4,40,29500,39500,915.4570013614677 +12,4,40,29500,40000,925.65401444575 +13,4,40,29500,40500,936.9348578520337 +14,4,40,29500,41000,948.3759339765711 +15,4,40,29500,41500,959.386491783636 +16,4,40,30000,38000,1169.8685711377334 +17,4,40,30000,38500,1166.2211505723928 +18,4,40,30000,39000,1167.702295374574 +19,4,40,30000,39500,1172.5517020611685 +20,4,40,30000,40000,1179.3820406408263 +21,4,40,30000,40500,1187.1698633839655 +22,4,40,30000,41000,1195.2047840919602 +23,4,40,30000,41500,1203.0241101248102 +24,4,40,30500,38000,1445.9591944684807 +25,4,40,30500,38500,1442.6632745483 +26,4,40,30500,39000,1443.1982444457385 +27,4,40,30500,39500,1446.2833842279929 +28,4,40,30500,40000,1450.9012120934779 +29,4,40,30500,40500,1456.295140290636 +30,4,40,30500,41000,1461.9350767569827 +31,4,40,30500,41500,1467.4715014446226 +32,4,40,31000,38000,1726.8744994061449 +33,4,40,31000,38500,1724.2679845375048 +34,4,40,31000,39000,1724.4550886870552 +35,4,40,31000,39500,1726.5124587129135 +36,4,40,31000,40000,1729.7061680616455 +37,4,40,31000,40500,1733.48893482641 +38,4,40,31000,41000,1737.4753558920438 +39,4,40,31000,41500,1741.4093763605517 +40,4,40,31500,38000,2004.1978135112938 +41,4,40,31500,38500,2002.2807839860222 +42,4,40,31500,39000,2002.3676405166086 +43,4,40,31500,39500,2003.797808439923 +44,4,40,31500,40000,2006.048051591001 +45,4,40,31500,40500,2008.7281679153625 +46,4,40,31500,41000,2011.5626384878237 +47,4,40,31500,41500,2014.3675286347284 +48,4,80,29000,38000,845.8197358579285 +49,4,80,29000,38500,763.5039795545781 +50,4,80,29000,39000,709.8529964173656 +51,4,80,29000,39500,679.4215539491266 +52,4,80,29000,40000,666.4876088521157 +53,4,80,29000,40500,665.978271760966 +54,4,80,29000,41000,673.7240200504901 +55,4,80,29000,41500,686.4763909417914 +56,4,80,29500,38000,1042.519415429413 +57,4,80,29500,38500,982.8097210678039 +58,4,80,29500,39000,942.2990207573541 +59,4,80,29500,39500,917.9550916645245 +60,4,80,29500,40000,906.3116029967189 +61,4,80,29500,40500,904.0326666308792 +62,4,80,29500,41000,908.1964630052729 +63,4,80,29500,41500,916.4222043837499 +64,4,80,30000,38000,1271.1030403496538 +65,4,80,30000,38500,1227.7527550544085 +66,4,80,30000,39000,1197.433957624904 +67,4,80,30000,39500,1178.447676126182 +68,4,80,30000,40000,1168.645219243497 +69,4,80,30000,40500,1165.7995210546096 +70,4,80,30000,41000,1167.8586496250396 +71,4,80,30000,41500,1173.0949214020527 +72,4,80,30500,38000,1520.8220402652044 +73,4,80,30500,38500,1489.2563260709424 +74,4,80,30500,39000,1466.8099189128857 +75,4,80,30500,39500,1452.4352624958806 +76,4,80,30500,40000,1444.7074679423818 +77,4,80,30500,40500,1442.0820578624343 +78,4,80,30500,41000,1443.099006489627 +79,4,80,30500,41500,1446.5106517200784 +80,4,80,31000,38000,1781.149136032395 +81,4,80,31000,38500,1758.2414369536502 +82,4,80,31000,39000,1741.891639711003 +83,4,80,31000,39500,1731.358661496594 +84,4,80,31000,40000,1725.6231647999593 +85,4,80,31000,40500,1723.5757174297378 +86,4,80,31000,41000,1724.1680229486278 +87,4,80,31000,41500,1726.5050840601884 +88,4,80,31500,38000,2042.8335948845602 +89,4,80,31500,38500,2026.3067503042414 +90,4,80,31500,39000,2014.5720701940838 +91,4,80,31500,39500,2007.0463766643977 +92,4,80,31500,40000,2002.9647983728314 +93,4,80,31500,40500,2001.5163951989875 +94,4,80,31500,41000,2001.9474217001339 +95,4,80,31500,41500,2003.6204088755821 +96,4,120,29000,38000,1176.0713512305115 +97,4,120,29000,38500,1016.8213383282462 +98,4,120,29000,39000,886.0136231565133 +99,4,120,29000,39500,789.0101180066036 +100,4,120,29000,40000,724.5420056133441 +101,4,120,29000,40500,686.6877602625062 +102,4,120,29000,41000,668.8129085873959 +103,4,120,29000,41500,665.1167761036883 +104,4,120,29500,38000,1263.887274509128 +105,4,120,29500,38500,1155.6528408872423 +106,4,120,29500,39000,1066.393539894248 +107,4,120,29500,39500,998.9931006471243 +108,4,120,29500,40000,952.36314487701 +109,4,120,29500,40500,923.4000293372077 +110,4,120,29500,41000,908.407361383214 +111,4,120,29500,41500,903.8136176328255 +112,4,120,30000,38000,1421.1418235449091 +113,4,120,30000,38500,1347.114022652679 +114,4,120,30000,39000,1285.686103704643 +115,4,120,30000,39500,1238.2456448658272 +116,4,120,30000,40000,1204.3526810790904 +117,4,120,30000,40500,1182.4272879027071 +118,4,120,30000,41000,1170.3447810121902 +119,4,120,30000,41500,1165.8422968073423 +120,4,120,30500,38000,1625.5588911535713 +121,4,120,30500,38500,1573.5546642859429 +122,4,120,30500,39000,1530.1592840718379 +123,4,120,30500,39500,1496.2087139473604 +124,4,120,30500,40000,1471.525855239756 +125,4,120,30500,40500,1455.2084749904016 +126,4,120,30500,41000,1445.9160840082027 +127,4,120,30500,41500,1442.1255377330835 +128,4,120,31000,38000,1855.8467211183756 +129,4,120,31000,38500,1818.4368412235558 +130,4,120,31000,39000,1787.25956706785 +131,4,120,31000,39500,1762.8169908546402 +132,4,120,31000,40000,1744.9825741661596 +133,4,120,31000,40500,1733.136625016882 +134,4,120,31000,41000,1726.3352245899828 +135,4,120,31000,41500,1723.492199933745 +136,4,120,31500,38000,2096.6479813687533 +137,4,120,31500,38500,2069.3606691038876 +138,4,120,31500,39000,2046.792043575205 +139,4,120,31500,39500,2029.2128703900223 +140,4,120,31500,40000,2016.4664599897606 +141,4,120,31500,40500,2008.054814885348 +142,4,120,31500,41000,2003.2622557140814 +143,4,120,31500,41500,2001.289784483679 +144,7,40,29000,38000,149.32898706737052 +145,7,40,29000,38500,161.04814413969586 +146,7,40,29000,39000,187.87801343005242 +147,7,40,29000,39500,223.00789161520424 +148,7,40,29000,40000,261.66779887964003 +149,7,40,29000,40500,300.676316191238 +150,7,40,29000,41000,338.04021206995765 +151,7,40,29000,41500,372.6191631389286 +152,7,40,29500,38000,276.6495061185777 +153,7,40,29500,38500,282.1304583501965 +154,7,40,29500,39000,300.91417483065254 +155,7,40,29500,39500,327.24304394350395 +156,7,40,29500,40000,357.0561976596432 +157,7,40,29500,40500,387.61662064170207 +158,7,40,29500,41000,417.1836349752378 +159,7,40,29500,41500,444.73705844573243 +160,7,40,30000,38000,448.0380830353589 +161,7,40,30000,38500,448.8094536459122 +162,7,40,30000,39000,460.77530593327293 +163,7,40,30000,39500,479.342874472736 +164,7,40,30000,40000,501.20694459059405 +165,7,40,30000,40500,524.0971649678811 +166,7,40,30000,41000,546.539334134893 +167,7,40,30000,41500,567.6447156158981 +168,7,40,30500,38000,657.9909416906933 +169,7,40,30500,38500,655.7465129488842 +170,7,40,30500,39000,662.5420970804985 +171,7,40,30500,39500,674.8914651553109 +172,7,40,30500,40000,690.2111920703564 +173,7,40,30500,40500,706.6833639709198 +174,7,40,30500,41000,723.0994507096715 +175,7,40,30500,41500,738.7096013891406 +176,7,40,31000,38000,899.1769906655776 +177,7,40,31000,38500,895.4391505892945 +178,7,40,31000,39000,898.7695629120826 +179,7,40,31000,39500,906.603316771593 +180,7,40,31000,40000,916.9811481373996 +181,7,40,31000,40500,928.4913367709245 +182,7,40,31000,41000,940.1744934710283 +183,7,40,31000,41500,951.4199286075984 +184,7,40,31500,38000,1163.093373675207 +185,7,40,31500,38500,1159.0457727559028 +186,7,40,31500,39000,1160.3831770028223 +187,7,40,31500,39500,1165.2451698296604 +188,7,40,31500,40000,1172.1768190340001 +189,7,40,31500,40500,1180.1105659428963 +190,7,40,31500,41000,1188.3083929833688 +191,7,40,31500,41500,1196.29112579565 +192,7,80,29000,38000,514.0332369183081 +193,7,80,29000,38500,329.3645784712966 +194,7,80,29000,39000,215.73000998706416 +195,7,80,29000,39500,162.37338399591852 +196,7,80,29000,40000,149.8401793263549 +197,7,80,29000,40500,162.96125998112578 +198,7,80,29000,41000,191.173279165834 +199,7,80,29000,41500,227.2781971491003 +200,7,80,29500,38000,623.559246695578 +201,7,80,29500,38500,448.60620511421484 +202,7,80,29500,39000,344.21940687907573 +203,7,80,29500,39500,292.9758707105001 +204,7,80,29500,40000,277.07670134364804 +205,7,80,29500,40500,283.5158840045542 +206,7,80,29500,41000,303.33951582820265 +207,7,80,29500,41500,330.43357046741954 +208,7,80,30000,38000,732.5907387079073 +209,7,80,30000,38500,593.1926567994672 +210,7,80,30000,39000,508.5638538704666 +211,7,80,30000,39500,464.47881763522037 +212,7,80,30000,40000,448.0394620671692 +213,7,80,30000,40500,449.64309860415494 +214,7,80,30000,41000,462.4490598612332 +215,7,80,30000,41500,481.6323506247537 +216,7,80,30500,38000,871.1163930229344 +217,7,80,30500,38500,771.1320563649375 +218,7,80,30500,39000,707.8872660015606 +219,7,80,30500,39500,672.6612145133173 +220,7,80,30500,40000,657.4974157809264 +221,7,80,30500,40500,656.0835852491216 +222,7,80,30500,41000,663.6006958125331 +223,7,80,30500,41500,676.460675405631 +224,7,80,31000,38000,1053.1852617390061 +225,7,80,31000,38500,984.3647109805877 +226,7,80,31000,39000,938.6158531749268 +227,7,80,31000,39500,911.4268280093535 +228,7,80,31000,40000,898.333365348419 +229,7,80,31000,40500,895.3996527486954 +230,7,80,31000,41000,899.3556288533885 +231,7,80,31000,41500,907.6180684887955 +232,7,80,31500,38000,1274.2255948763498 +233,7,80,31500,38500,1226.5236809533717 +234,7,80,31500,39000,1193.4538731398666 +235,7,80,31500,39500,1172.8105398345213 +236,7,80,31500,40000,1162.0692230240734 +237,7,80,31500,40500,1158.7461521476607 +238,7,80,31500,41000,1160.6173577210805 +239,7,80,31500,41500,1165.840315694716 +240,7,120,29000,38000,1325.2409732290193 +241,7,120,29000,38500,900.8063148840154 +242,7,120,29000,39000,629.9300352098937 +243,7,120,29000,39500,413.81648033893424 +244,7,120,29000,40000,257.3116751690404 +245,7,120,29000,40500,177.89217179438947 +246,7,120,29000,41000,151.58366848473491 +247,7,120,29000,41500,157.56967437251706 +248,7,120,29500,38000,1211.2807882170853 +249,7,120,29500,38500,956.936161969002 +250,7,120,29500,39000,753.3050086992201 +251,7,120,29500,39500,528.2452647799327 +252,7,120,29500,40000,382.62610532894917 +253,7,120,29500,40500,308.44199089882375 +254,7,120,29500,41000,280.3893024671524 +255,7,120,29500,41500,280.4028092582749 +256,7,120,30000,38000,1266.5740351143413 +257,7,120,30000,38500,1084.3028700477778 +258,7,120,30000,39000,834.2392498526193 +259,7,120,30000,39500,650.7560171314304 +260,7,120,30000,40000,537.7846910878052 +261,7,120,30000,40500,477.3001078155485 +262,7,120,30000,41000,451.6865380286754 +263,7,120,30000,41500,448.14911508024613 +264,7,120,30500,38000,1319.6603196780936 +265,7,120,30500,38500,1102.3027489012372 +266,7,120,30500,39000,931.2523583659847 +267,7,120,30500,39500,807.0833484596384 +268,7,120,30500,40000,727.4852710400268 +269,7,120,30500,40500,682.1437030344305 +270,7,120,30500,41000,660.7859329989657 +271,7,120,30500,41500,655.6001132492668 +272,7,120,31000,38000,1330.5306924865326 +273,7,120,31000,38500,1195.9190861202942 +274,7,120,31000,39000,1086.0328080422887 +275,7,120,31000,39500,1005.4160637517409 +276,7,120,31000,40000,951.2021706290612 +277,7,120,31000,40500,918.1457644271304 +278,7,120,31000,41000,901.0511005554887 +279,7,120,31000,41500,895.4599964465793 +280,7,120,31500,38000,1447.8365822059013 +281,7,120,31500,38500,1362.3417347939844 +282,7,120,31500,39000,1292.382727215108 +283,7,120,31500,39500,1239.1826828976662 +284,7,120,31500,40000,1201.6474412465277 +285,7,120,31500,40500,1177.5235955796813 +286,7,120,31500,41000,1164.1761722345295 +287,7,120,31500,41500,1158.9997785002718 +288,10,40,29000,38000,33.437068437082054 +289,10,40,29000,38500,58.471249815534996 +290,10,40,29000,39000,101.41937628542912 +291,10,40,29000,39500,153.80690200519626 +292,10,40,29000,40000,209.66451461551316 +293,10,40,29000,40500,265.03070792175197 +294,10,40,29000,41000,317.46079310177566 +295,10,40,29000,41500,365.59950388342645 +296,10,40,29500,38000,70.26818405688635 +297,10,40,29500,38500,87.96463718548947 +298,10,40,29500,39000,122.58188233160993 +299,10,40,29500,39500,166.2478945807132 +300,10,40,29500,40000,213.48669617414316 +301,10,40,29500,40500,260.67953961944477 +302,10,40,29500,41000,305.5877041218316 +303,10,40,29500,41500,346.95612213021155 +304,10,40,30000,38000,153.67588703371362 +305,10,40,30000,38500,164.07504103479005 +306,10,40,30000,39000,190.0800160661499 +307,10,40,30000,39500,224.61382980242837 +308,10,40,30000,40000,262.79232847382445 +309,10,40,30000,40500,301.38687703450415 +310,10,40,30000,41000,338.38536686093164 +311,10,40,30000,41500,372.6399011703545 +312,10,40,30500,38000,284.2936286531718 +313,10,40,30500,38500,288.4690608277705 +314,10,40,30500,39000,306.44667517621144 +315,10,40,30500,39500,332.20122250191986 +316,10,40,30500,40000,361.5566690083291 +317,10,40,30500,40500,391.72755224929614 +318,10,40,30500,41000,420.95317535960476 +319,10,40,30500,41500,448.2049230608669 +320,10,40,31000,38000,459.03140021766137 +321,10,40,31000,38500,458.71477027519967 +322,10,40,31000,39000,469.9910751800656 +323,10,40,31000,39500,488.05850105225426 +324,10,40,31000,40000,509.5204701455629 +325,10,40,31000,40500,532.0674969691778 +326,10,40,31000,41000,554.2088430693509 +327,10,40,31000,41500,575.0485839499048 +328,10,40,31500,38000,672.2476845983564 +329,10,40,31500,38500,669.2240508488649 +330,10,40,31500,39000,675.4956226836405 +331,10,40,31500,39500,687.447764319295 +332,10,40,31500,40000,702.4395430742891 +333,10,40,31500,40500,718.6279487347668 +334,10,40,31500,41000,734.793684592168 +335,10,40,31500,41500,750.1821072409286 +336,10,80,29000,38000,387.7617282731497 +337,10,80,29000,38500,195.33642612593002 +338,10,80,29000,39000,82.7306931465102 +339,10,80,29000,39500,35.13436471793541 +340,10,80,29000,40000,33.521138659248706 +341,10,80,29000,40500,61.47395975053128 +342,10,80,29000,41000,106.71403229340167 +343,10,80,29000,41500,160.56068704487473 +344,10,80,29500,38000,459.63404601804103 +345,10,80,29500,38500,258.7453720995899 +346,10,80,29500,39000,135.96435731320256 +347,10,80,29500,39500,80.2685095017944 +348,10,80,29500,40000,70.86302366453106 +349,10,80,29500,40500,90.43203026480438 +350,10,80,29500,41000,126.7844695901737 +351,10,80,29500,41500,171.63682876805044 +352,10,80,30000,38000,564.1463320344325 +353,10,80,30000,38500,360.75718124523866 +354,10,80,30000,39000,231.70119191254307 +355,10,80,30000,39500,170.74752201483128 +356,10,80,30000,40000,154.7149036950422 +357,10,80,30000,40500,166.10596450541493 +358,10,80,30000,41000,193.3351721194443 +359,10,80,30000,41500,228.78394172417038 +360,10,80,30500,38000,689.6797223218513 +361,10,80,30500,38500,484.8023695265838 +362,10,80,30500,39000,363.5979340028588 +363,10,80,30500,39500,304.67857102688225 +364,10,80,30500,40000,285.29210000833734 +365,10,80,30500,40500,290.0135917456113 +366,10,80,30500,41000,308.8672169492536 +367,10,80,30500,41500,335.3210332569182 +368,10,80,31000,38000,789.946106942773 +369,10,80,31000,38500,625.7722360026959 +370,10,80,31000,39000,528.6063264942235 +371,10,80,31000,39500,478.6863763478618 +372,10,80,31000,40000,459.5026243189753 +373,10,80,31000,40500,459.6982093164963 +374,10,80,31000,41000,471.6790024321937 +375,10,80,31000,41500,490.3034492109124 +376,10,80,31500,38000,912.3540488244158 +377,10,80,31500,38500,798.2135101409633 +378,10,80,31500,39000,727.746684419146 +379,10,80,31500,39500,689.0119464356724 +380,10,80,31500,40000,672.0757202772029 +381,10,80,31500,40500,669.678339553036 +382,10,80,31500,41000,676.5761221409929 +383,10,80,31500,41500,688.9934449650118 +384,10,120,29000,38000,1155.1165164624408 +385,10,120,29000,38500,840.2641727088946 +386,10,120,29000,39000,506.9102636732852 +387,10,120,29000,39500,265.5278912452038 +388,10,120,29000,40000,116.39516513179322 +389,10,120,29000,40500,45.2088092745619 +390,10,120,29000,41000,30.22267557153353 +391,10,120,29000,41500,51.06063746392809 +392,10,120,29500,38000,1343.7868459826054 +393,10,120,29500,38500,977.9852373227346 +394,10,120,29500,39000,594.632756549817 +395,10,120,29500,39500,346.2478773329187 +396,10,120,29500,40000,180.23082247413407 +397,10,120,29500,40500,95.81649989178923 +398,10,120,29500,41000,71.0837801649128 +399,10,120,29500,41500,82.84289818279714 +400,10,120,30000,38000,1532.9333545384934 +401,10,120,30000,38500,1012.2223350568845 +402,10,120,30000,39000,688.4884716222766 +403,10,120,30000,39500,464.6206903113392 +404,10,120,30000,40000,283.5644748300334 +405,10,120,30000,40500,190.27593217865416 +406,10,120,30000,41000,158.0192279691727 +407,10,120,30000,41500,161.3611926772337 +408,10,120,30500,38000,1349.3785399811063 +409,10,120,30500,38500,1014.785480110738 +410,10,120,30500,39000,843.0316833766408 +411,10,120,30500,39500,589.4543896730125 +412,10,120,30500,40000,412.3358512291996 +413,10,120,30500,40500,324.11715620464133 +414,10,120,30500,41000,290.17588242984766 +415,10,120,30500,41500,287.56857384673356 +416,10,120,31000,38000,1328.0973931040146 +417,10,120,31000,38500,1216.5659656437845 +418,10,120,31000,39000,928.4831767181619 +419,10,120,31000,39500,700.3115484040329 +420,10,120,31000,40000,565.0876352458171 +421,10,120,31000,40500,494.44016026435037 +422,10,120,31000,41000,464.38005437182983 +423,10,120,31000,41500,458.7614573733091 +424,10,120,31500,38000,1473.1154650008834 +425,10,120,31500,38500,1195.943614951571 +426,10,120,31500,39000,990.2486604382486 +427,10,120,31500,39500,843.1390407497395 +428,10,120,31500,40000,751.2746391170706 +429,10,120,31500,40500,700.215375503209 +430,10,120,31500,41000,676.1585052687219 +431,10,120,31500,41500,669.5907920932743 +432,13,40,29000,38000,49.96352152045025 +433,13,40,29000,38500,83.75104994958261 +434,13,40,29000,39000,136.8176091795391 +435,13,40,29000,39500,199.91486685466407 +436,13,40,29000,40000,266.4367154860076 +437,13,40,29000,40500,331.97224579940524 +438,13,40,29000,41000,393.8001583706036 +439,13,40,29000,41500,450.42425363084493 +440,13,40,29500,38000,29.775721038786923 +441,13,40,29500,38500,57.37673742631121 +442,13,40,29500,39000,103.49161398239501 +443,13,40,29500,39500,159.3058253852367 +444,13,40,29500,40000,218.60083223764073 +445,13,40,29500,40500,277.2507278183831 +446,13,40,29500,41000,332.7141278886951 +447,13,40,29500,41500,383.58832292300576 +448,13,40,30000,38000,47.72263852005472 +449,13,40,30000,38500,68.07581028940402 +450,13,40,30000,39000,106.13974628945516 +451,13,40,30000,39500,153.58449949683063 +452,13,40,30000,40000,204.62393623358633 +453,13,40,30000,40500,255.44513025602419 +454,13,40,30000,41000,303.69954914051766 +455,13,40,30000,41500,348.0803709720354 +456,13,40,30500,38000,110.9331168284094 +457,13,40,30500,38500,123.63361262704746 +458,13,40,30500,39000,153.02654433825705 +459,13,40,30500,39500,191.40769947472756 +460,13,40,30500,40000,233.503841403055 +461,13,40,30500,40500,275.8557790922913 +462,13,40,30500,41000,316.32529882763697 +463,13,40,30500,41500,353.7060432094809 +464,13,40,31000,38000,221.90608823073939 +465,13,40,31000,38500,227.67026441593657 +466,13,40,31000,39000,248.62107049869064 +467,13,40,31000,39500,277.9507605389158 +468,13,40,31000,40000,311.0267471957685 +469,13,40,31000,40500,344.8024031161673 +470,13,40,31000,41000,377.3761144228052 +471,13,40,31000,41500,407.6529635071056 +472,13,40,31500,38000,378.8738382757093 +473,13,40,31500,38500,379.39748335944216 +474,13,40,31500,39000,393.01223361732553 +475,13,40,31500,39500,414.10238059122855 +476,13,40,31500,40000,438.8024282436204 +477,13,40,31500,40500,464.5348067190265 +478,13,40,31500,41000,489.6621039898805 +479,13,40,31500,41500,513.2163939332803 +480,13,80,29000,38000,364.387588581215 +481,13,80,29000,38500,184.2902007673634 +482,13,80,29000,39000,81.57192155036655 +483,13,80,29000,39500,42.54811210095659 +484,13,80,29000,40000,49.897338772663076 +485,13,80,29000,40500,87.84229516509882 +486,13,80,29000,41000,143.85451969447664 +487,13,80,29000,41500,208.71467984917848 +488,13,80,29500,38000,382.5794635435733 +489,13,80,29500,38500,188.38619353711718 +490,13,80,29500,39000,75.75749359688277 +491,13,80,29500,39500,29.27891251986562 +492,13,80,29500,40000,29.794874961934568 +493,13,80,29500,40500,60.654888662698205 +494,13,80,29500,41000,109.25801388824325 +495,13,80,29500,41500,166.6311093454692 +496,13,80,30000,38000,448.97795526074816 +497,13,80,30000,38500,238.44530107604737 +498,13,80,30000,39000,112.34545890264337 +499,13,80,30000,39500,56.125871791222835 +500,13,80,30000,40000,48.29987461781518 +501,13,80,30000,40500,70.7900626637678 +502,13,80,30000,41000,110.76865376691964 +503,13,80,30000,41500,159.50197316936024 +504,13,80,30500,38000,547.7818730461195 +505,13,80,30500,38500,332.92604070423494 +506,13,80,30500,39000,193.80760050280742 +507,13,80,30500,39500,128.3457644087917 +508,13,80,30500,40000,112.23915895822442 +509,13,80,30500,40500,125.96369396512564 +510,13,80,30500,41000,156.67918617660013 +511,13,80,30500,41500,196.05195109523765 +512,13,80,31000,38000,682.8591931963246 +513,13,80,31000,38500,457.56562267948556 +514,13,80,31000,39000,313.6380169123524 +515,13,80,31000,39500,245.13531819580908 +516,13,80,31000,40000,223.54473391202873 +517,13,80,31000,40500,229.60752111202834 +518,13,80,31000,41000,251.42377424735136 +519,13,80,31000,41500,281.48720903016886 +520,13,80,31500,38000,807.925638050234 +521,13,80,31500,38500,588.686585641994 +522,13,80,31500,39000,464.0488586698228 +523,13,80,31500,39500,402.69214492641095 +524,13,80,31500,40000,380.13626165363934 +525,13,80,31500,40500,380.8064948609387 +526,13,80,31500,41000,395.05186915919086 +527,13,80,31500,41500,416.70193045600774 +528,13,120,29000,38000,1068.8279454397398 +529,13,120,29000,38500,743.0012805963486 +530,13,120,29000,39000,451.2538301167544 +531,13,120,29000,39500,235.4154251166075 +532,13,120,29000,40000,104.73720814447498 +533,13,120,29000,40500,46.91983990671749 +534,13,120,29000,41000,42.81092192562316 +535,13,120,29000,41500,74.33530639171506 +536,13,120,29500,38000,1133.1178848710972 +537,13,120,29500,38500,824.0745323788527 +538,13,120,29500,39000,499.10867111401996 +539,13,120,29500,39500,256.1626809904186 +540,13,120,29500,40000,107.68599585294751 +541,13,120,29500,40500,38.18533662516749 +542,13,120,29500,41000,25.499608203619154 +543,13,120,29500,41500,49.283537699300375 +544,13,120,30000,38000,1292.409871290162 +545,13,120,30000,38500,994.669572829704 +546,13,120,30000,39000,598.9783697712826 +547,13,120,30000,39500,327.47348408537925 +548,13,120,30000,40000,156.82634841081907 +549,13,120,30000,40500,71.30833688875883 +550,13,120,30000,41000,47.72389750130817 +551,13,120,30000,41500,62.1982461882982 +552,13,120,30500,38000,1585.8797221278146 +553,13,120,30500,38500,1144.66688416451 +554,13,120,30500,39000,692.6651441690645 +555,13,120,30500,39500,441.98837639874046 +556,13,120,30500,40000,251.56311435857728 +557,13,120,30500,40500,149.79670413140468 +558,13,120,30500,41000,115.52645596043719 +559,13,120,30500,41500,120.44019473389324 +560,13,120,31000,38000,1702.7625866892163 +561,13,120,31000,38500,1071.7854750250656 +562,13,120,31000,39000,807.8943299034604 +563,13,120,31000,39500,588.672223513561 +564,13,120,31000,40000,376.44658358671404 +565,13,120,31000,40500,269.2159719426485 +566,13,120,31000,41000,229.41660529009877 +567,13,120,31000,41500,226.78274707181976 +568,13,120,31500,38000,1331.3523701291767 +569,13,120,31500,38500,1151.2055268669133 +570,13,120,31500,39000,1006.811285091974 +571,13,120,31500,39500,702.0053094629535 +572,13,120,31500,40000,515.9081891614829 +573,13,120,31500,40500,423.8652275555525 +574,13,120,31500,41000,386.4939696097151 +575,13,120,31500,41500,379.8118453367429 +576,16,40,29000,38000,106.1025746852808 +577,16,40,29000,38500,145.32590128581407 +578,16,40,29000,39000,204.74804378224422 +579,16,40,29000,39500,274.6339266648551 +580,16,40,29000,40000,347.9667393938497 +581,16,40,29000,40500,420.03753452490974 +582,16,40,29000,41000,487.9353932879741 +583,16,40,29000,41500,550.0623063219693 +584,16,40,29500,38000,54.65040870471303 +585,16,40,29500,38500,88.94089091627293 +586,16,40,29500,39000,142.72223808288405 +587,16,40,29500,39500,206.63598763907422 +588,16,40,29500,40000,273.99851593521134 +589,16,40,29500,40500,340.34861536649436 +590,16,40,29500,41000,402.935270882596 +591,16,40,29500,41500,460.2471155081633 +592,16,40,30000,38000,29.788548081995298 +593,16,40,30000,38500,57.96323252610644 +594,16,40,30000,39000,104.92815906834525 +595,16,40,30000,39500,161.71867032726158 +596,16,40,30000,40000,222.01677586338877 +597,16,40,30000,40500,281.6349465235367 +598,16,40,30000,41000,337.99683241119567 +599,16,40,30000,41500,389.68271710858414 +600,16,40,30500,38000,42.06569536892785 +601,16,40,30500,38500,62.95145274276575 +602,16,40,30500,39000,101.93860830594608 +603,16,40,30500,39500,150.47910837525734 +604,16,40,30500,40000,202.65388851823258 +605,16,40,30500,40500,254.5724108541227 +606,16,40,30500,41000,303.84403622726694 +607,16,40,30500,41500,349.1422884543064 +608,16,40,31000,38000,99.21707896667829 +609,16,40,31000,38500,112.24153596941301 +610,16,40,31000,39000,142.5186177618655 +611,16,40,31000,39500,182.02836955332134 +612,16,40,31000,40000,225.3201896575212 +613,16,40,31000,40500,268.83705389232614 +614,16,40,31000,41000,310.3895932135811 +615,16,40,31000,41500,348.7480165565453 +616,16,40,31500,38000,204.30418825821732 +617,16,40,31500,38500,210.0759235359138 +618,16,40,31500,39000,231.7643258544752 +619,16,40,31500,39500,262.1512494310348 +620,16,40,31500,40000,296.3864127264238 +621,16,40,31500,40500,331.30743171999035 +622,16,40,31500,41000,364.95322314895554 +623,16,40,31500,41500,396.20142191205844 +624,16,80,29000,38000,399.5975649320935 +625,16,80,29000,38500,225.6318269911425 +626,16,80,29000,39000,127.97354075513151 +627,16,80,29000,39500,93.73584101549991 +628,16,80,29000,40000,106.43084032022394 +629,16,80,29000,40500,150.51245762256931 +630,16,80,29000,41000,213.24213500046466 +631,16,80,29000,41500,285.0426423013882 +632,16,80,29500,38000,371.37706087096393 +633,16,80,29500,38500,189.77150413822454 +634,16,80,29500,39000,86.22375488959844 +635,16,80,29500,39500,46.98714814001572 +636,16,80,29500,40000,54.596900621760675 +637,16,80,29500,40500,93.12033833747024 +638,16,80,29500,41000,149.89341227947025 +639,16,80,29500,41500,215.5937000584367 +640,16,80,30000,38000,388.43657991253195 +641,16,80,30000,38500,190.77121362008674 +642,16,80,30000,39000,76.28535232335287 +643,16,80,30000,39500,29.152860363695716 +644,16,80,30000,40000,29.820972887404942 +645,16,80,30000,40500,61.320203047752464 +646,16,80,30000,41000,110.82086782062603 +647,16,80,30000,41500,169.197767615573 +648,16,80,30500,38000,458.8964339917103 +649,16,80,30500,38500,239.547928886725 +650,16,80,30500,39000,109.02338779317503 +651,16,80,30500,39500,50.888746196140914 +652,16,80,30500,40000,42.73606982375976 +653,16,80,30500,40500,65.75935122724029 +654,16,80,30500,41000,106.68884313872147 +655,16,80,30500,41500,156.54100549486617 +656,16,80,31000,38000,561.7385153195615 +657,16,80,31000,38500,335.5692026144635 +658,16,80,31000,39000,188.0383015831574 +659,16,80,31000,39500,118.2318539104416 +660,16,80,31000,40000,100.81000168801492 +661,16,80,31000,40500,114.72014539486217 +662,16,80,31000,41000,146.2992492326178 +663,16,80,31000,41500,186.8074429488408 +664,16,80,31500,38000,697.9937997454152 +665,16,80,31500,38500,466.42234442578484 +666,16,80,31500,39000,306.52125608515166 +667,16,80,31500,39500,230.54692639209762 +668,16,80,31500,40000,206.461121102699 +669,16,80,31500,40500,212.23429887269359 +670,16,80,31500,41000,234.70913795495554 +671,16,80,31500,41500,265.8143069252357 +672,16,120,29000,38000,1085.688903883652 +673,16,120,29000,38500,750.2887000017752 +674,16,120,29000,39000,469.92662852990964 +675,16,120,29000,39500,267.1560282754928 +676,16,120,29000,40000,146.06299930062625 +677,16,120,29000,40500,95.28836772053619 +678,16,120,29000,41000,97.41466545178946 +679,16,120,29000,41500,135.3804131941845 +680,16,120,29500,38000,1079.5576154477903 +681,16,120,29500,38500,751.2932384998761 +682,16,120,29500,39000,458.27083477307207 +683,16,120,29500,39500,240.9658024131812 +684,16,120,29500,40000,109.3801465044384 +685,16,120,29500,40500,51.274139057659724 +686,16,120,29500,41000,47.36446629605638 +687,16,120,29500,41500,79.42944320845996 +688,16,120,30000,38000,1139.3792936518537 +689,16,120,30000,38500,833.7979589668842 +690,16,120,30000,39000,507.805443202025 +691,16,120,30000,39500,259.93892964607977 +692,16,120,30000,40000,108.7341499557062 +693,16,120,30000,40500,38.152937143498605 +694,16,120,30000,41000,25.403985123518716 +695,16,120,30000,41500,49.72822589160786 +696,16,120,30500,38000,1285.0396277304772 +697,16,120,30500,38500,1025.254169031627 +698,16,120,30500,39000,622.5890550779666 +699,16,120,30500,39500,333.3353043756717 +700,16,120,30500,40000,155.70268128051293 +701,16,120,30500,40500,66.84125446522368 +702,16,120,30500,41000,42.25187049753978 +703,16,120,30500,41500,56.98314898830595 +704,16,120,31000,38000,1595.7993459811262 +705,16,120,31000,38500,1252.8886556470425 +706,16,120,31000,39000,731.4408383874198 +707,16,120,31000,39500,451.0090473423308 +708,16,120,31000,40000,251.5086563526081 +709,16,120,31000,40500,141.8915050063955 +710,16,120,31000,41000,104.67474675582574 +711,16,120,31000,41500,109.1609567535697 +712,16,120,31500,38000,1942.3896021770768 +713,16,120,31500,38500,1197.207050908449 +714,16,120,31500,39000,812.6818768064074 +715,16,120,31500,39500,611.45532452889 +716,16,120,31500,40000,380.63642711770643 +717,16,120,31500,40500,258.5514125337487 +718,16,120,31500,41000,213.48518421250665 +719,16,120,31500,41500,209.58134396574906 +720,19,40,29000,38000,169.3907733115706 +721,19,40,29000,38500,212.23331960093145 +722,19,40,29000,39000,275.9376503672959 +723,19,40,29000,39500,350.4301397081139 +724,19,40,29000,40000,428.40863665493924 +725,19,40,29000,40500,504.955113902399 +726,19,40,29000,41000,577.023450987656 +727,19,40,29000,41500,642.9410032211753 +728,19,40,29500,38000,102.40889356493292 +729,19,40,29500,38500,141.19036226103668 +730,19,40,29500,39000,200.19333708701748 +731,19,40,29500,39500,269.6750686488757 +732,19,40,29500,40000,342.6217886299377 +733,19,40,29500,40500,414.33044375626207 +734,19,40,29500,41000,481.89521316730713 +735,19,40,29500,41500,543.7211700546151 +736,19,40,30000,38000,51.95330426445395 +737,19,40,30000,38500,85.69656829127965 +738,19,40,30000,39000,138.98376466247876 +739,19,40,30000,39500,202.43251598105033 +740,19,40,30000,40000,269.3557903452929 +741,19,40,30000,40500,335.2960133312316 +742,19,40,30000,41000,397.50658847538665 +743,19,40,30000,41500,454.47903112410967 +744,19,40,30500,38000,28.864802790801026 +745,19,40,30500,38500,56.32899754732796 +746,19,40,30500,39000,102.69825523352162 +747,19,40,30500,39500,158.95118263535466 +748,19,40,30500,40000,218.75241957992617 +749,19,40,30500,40500,277.9122290233915 +750,19,40,30500,41000,333.8561815041273 +751,19,40,30500,41500,385.1662652901447 +752,19,40,31000,38000,43.72359701781447 +753,19,40,31000,38500,63.683967347844224 +754,19,40,31000,39000,101.95579433282329 +755,19,40,31000,39500,149.8826019475827 +756,19,40,31000,40000,201.50605279789198 +757,19,40,31000,40500,252.92391570754876 +758,19,40,31000,41000,301.7431453727685 +759,19,40,31000,41500,346.6368192781496 +760,19,40,31500,38000,104.05710998615942 +761,19,40,31500,38500,115.95783594434451 +762,19,40,31500,39000,145.42181873662554 +763,19,40,31500,39500,184.26373455825217 +764,19,40,31500,40000,226.97066340897095 +765,19,40,31500,40500,269.96403356902357 +766,19,40,31500,41000,311.04753558871505 +767,19,40,31500,41500,348.98866332680115 +768,19,80,29000,38000,453.1314944429312 +769,19,80,29000,38500,281.24067760117225 +770,19,80,29000,39000,185.83730378881882 +771,19,80,29000,39500,154.25726305915472 +772,19,80,29000,40000,170.2912737797755 +773,19,80,29000,40500,218.38979299191152 +774,19,80,29000,41000,285.604024444273 +775,19,80,29000,41500,362.0858325427657 +776,19,80,29500,38000,400.06299682217264 +777,19,80,29500,38500,224.41725666435008 +778,19,80,29500,39000,125.58476107530382 +779,19,80,29500,39500,90.55733834394478 +780,19,80,29500,40000,102.67519971027264 +781,19,80,29500,40500,146.27807815967392 +782,19,80,29500,41000,208.57372904155937 +783,19,80,29500,41500,279.9669583078214 +784,19,80,30000,38000,376.1594584816549 +785,19,80,30000,38500,191.30452808298463 +786,19,80,30000,39000,85.63116084217559 +787,19,80,30000,39500,45.10487847849711 +788,19,80,30000,40000,51.88389644342952 +789,19,80,30000,40500,89.78942817703852 +790,19,80,30000,41000,146.0393555385696 +791,19,80,30000,41500,211.26567367707352 +792,19,80,30500,38000,401.874315275947 +793,19,80,30500,38500,197.55305366608133 +794,19,80,30500,39000,79.00348967857379 +795,19,80,30500,39500,29.602719961568614 +796,19,80,30500,40000,28.980451378502487 +797,19,80,30500,40500,59.63541802023186 +798,19,80,30500,41000,108.48607655362268 +799,19,80,30500,41500,166.30589286399507 +800,19,80,31000,38000,484.930958445979 +801,19,80,31000,38500,254.27552635537404 +802,19,80,31000,39000,116.75543721560439 +803,19,80,31000,39500,54.77547840250418 +804,19,80,31000,40000,44.637472658824976 +805,19,80,31000,40500,66.50466903927668 +806,19,80,31000,41000,106.62737262508298 +807,19,80,31000,41500,155.8310688191254 +808,19,80,31500,38000,595.6094306603337 +809,19,80,31500,38500,359.60040819463063 +810,19,80,31500,39000,201.85328967228585 +811,19,80,31500,39500,126.24442464793601 +812,19,80,31500,40000,106.07388975142673 +813,19,80,31500,40500,118.52358345403363 +814,19,80,31500,41000,149.1597537162607 +815,19,80,31500,41500,188.94964975523197 +816,19,120,29000,38000,1133.9213841599772 +817,19,120,29000,38500,793.9759807804692 +818,19,120,29000,39000,516.5580425563733 +819,19,120,29000,39500,318.60172051726147 +820,19,120,29000,40000,201.662212274693 +821,19,120,29000,40500,154.47522945829064 +822,19,120,29000,41000,160.28049502033574 +823,19,120,29000,41500,202.35345983501588 +824,19,120,29500,38000,1091.6343400395158 +825,19,120,29500,38500,754.9332443184217 +826,19,120,29500,39000,472.1777992591152 +827,19,120,29500,39500,267.03951846894995 +828,19,120,29500,40000,144.25558152688114 +829,19,120,29500,40500,92.40384156679512 +830,19,120,29500,41000,93.81833253459942 +831,19,120,29500,41500,131.24753560710644 +832,19,120,30000,38000,1092.719296892266 +833,19,120,30000,38500,764.7065490850255 +834,19,120,30000,39000,467.2268758064373 +835,19,120,30000,39500,244.9367732985332 +836,19,120,30000,40000,110.00996333393202 +837,19,120,30000,40500,49.96381544207811 +838,19,120,30000,41000,44.9298739569088 +839,19,120,30000,41500,76.25447129089613 +840,19,120,30500,38000,1160.6160120981158 +841,19,120,30500,38500,865.5953188304933 +842,19,120,30500,39000,531.1657093741892 +843,19,120,30500,39500,271.98520008106277 +844,19,120,30500,40000,114.03616090967407 +845,19,120,30500,40500,39.74252227099571 +846,19,120,30500,41000,25.07176465285551 +847,19,120,30500,41500,48.298794094852724 +848,19,120,31000,38000,1304.8870694342509 +849,19,120,31000,38500,1089.6854636757826 +850,19,120,31000,39000,668.6632735260521 +851,19,120,31000,39500,356.7751012890747 +852,19,120,31000,40000,168.32491564142487 +853,19,120,31000,40500,72.82648063377391 +854,19,120,31000,41000,45.02326687759286 +855,19,120,31000,41500,58.13111530831655 +856,19,120,31500,38000,1645.2697164013964 +857,19,120,31500,38500,1373.859712069864 +858,19,120,31500,39000,787.3948673670299 +859,19,120,31500,39500,483.60546305948367 +860,19,120,31500,40000,273.4285373433001 +861,19,120,31500,40500,153.21079535396908 +862,19,120,31500,41000,111.21299419905313 +863,19,120,31500,41500,113.52006337929113 +864,22,40,29000,38000,229.2032513971666 +865,22,40,29000,38500,274.65023153674116 +866,22,40,29000,39000,341.4424739822062 +867,22,40,29000,39500,419.2624324130753 +868,22,40,29000,40000,500.6022690006133 +869,22,40,29000,40500,580.3923016374031 +870,22,40,29000,41000,655.4874207991389 +871,22,40,29000,41500,724.1595537770351 +872,22,40,29500,38000,155.45206306046595 +873,22,40,29500,38500,197.41588482427002 +874,22,40,29500,39000,260.1641484982308 +875,22,40,29500,39500,333.666918810689 +876,22,40,29500,40000,410.66541588422854 +877,22,40,29500,40500,486.276072112155 +878,22,40,29500,41000,557.4760464927683 +879,22,40,29500,41500,622.6057687448293 +880,22,40,30000,38000,90.70026588811803 +881,22,40,30000,38500,128.41239603755494 +882,22,40,30000,39000,186.27261386900233 +883,22,40,30000,39500,254.5802373859711 +884,22,40,30000,40000,326.3686182341553 +885,22,40,30000,40500,396.9735001502319 +886,22,40,30000,41000,463.5155278718613 +887,22,40,30000,41500,524.414569320113 +888,22,40,30500,38000,44.551475763397946 +889,22,40,30500,38500,76.95264448905411 +890,22,40,30500,39000,128.85898727872572 +891,22,40,30500,39500,190.91422001003792 +892,22,40,30500,40000,256.4755613806196 +893,22,40,30500,40500,321.125224208803 +894,22,40,30500,41000,382.14434919800453 +895,22,40,30500,41500,438.03974322333033 +896,22,40,31000,38000,28.101321546315717 +897,22,40,31000,38500,53.867829756398805 +898,22,40,31000,39000,98.57619184859544 +899,22,40,31000,39500,153.19473192134507 +900,22,40,31000,40000,211.4202434313414 +901,22,40,31000,40500,269.09905982026265 +902,22,40,31000,41000,323.68306330754416 +903,22,40,31000,41500,373.76836451736045 +904,22,40,31500,38000,51.648288279447364 +905,22,40,31500,38500,69.56074881661863 +906,22,40,31500,39000,105.91402675097291 +907,22,40,31500,39500,151.99456204656389 +908,22,40,31500,40000,201.85995274525234 +909,22,40,31500,40500,251.63807959916412 +910,22,40,31500,41000,298.9593498669657 +911,22,40,31500,41500,342.50888994628025 +912,22,80,29000,38000,507.5440336860194 +913,22,80,29000,38500,336.42019672232965 +914,22,80,29000,39000,242.21016116765423 +915,22,80,29000,39500,212.33396533224905 +916,22,80,29000,40000,230.67632355958136 +917,22,80,29000,40500,281.6224662955561 +918,22,80,29000,41000,352.0457411487133 +919,22,80,29000,41500,431.89288175778637 +920,22,80,29500,38000,443.2889283037078 +921,22,80,29500,38500,270.0648237630224 +922,22,80,29500,39000,173.57666711629645 +923,22,80,29500,39500,141.06258420240613 +924,22,80,29500,40000,156.18412870159142 +925,22,80,29500,40500,203.33105261575707 +926,22,80,29500,41000,269.5552387411201 +927,22,80,29500,41500,345.03801326123767 +928,22,80,30000,38000,395.34177505602497 +929,22,80,30000,38500,217.11094192826982 +930,22,80,30000,39000,116.38535634181476 +931,22,80,30000,39500,79.94742924888467 +932,22,80,30000,40000,90.84706550421288 +933,22,80,30000,40500,133.26308067939766 +934,22,80,30000,41000,194.36064414396228 +935,22,80,30000,41500,264.56059537656466 +936,22,80,30500,38000,382.0341866812038 +937,22,80,30500,38500,191.65621311671836 +938,22,80,30500,39000,82.3318677587146 +939,22,80,30500,39500,39.44606931321677 +940,22,80,30500,40000,44.476166488763134 +941,22,80,30500,40500,80.84561981845566 +942,22,80,30500,41000,135.62459431793735 +943,22,80,30500,41500,199.42208168600175 +944,22,80,31000,38000,425.5181957619983 +945,22,80,31000,38500,210.2667219741389 +946,22,80,31000,39000,84.97041062888985 +947,22,80,31000,39500,31.593073529038755 +948,22,80,31000,40000,28.407154164211214 +949,22,80,31000,40500,57.05446633976857 +950,22,80,31000,41000,104.10423883907688 +951,22,80,31000,41500,160.23135976433713 +952,22,80,31500,38000,527.5015417150911 +953,22,80,31500,38500,282.29650611769665 +954,22,80,31500,39000,134.62881845323489 +955,22,80,31500,39500,66.62736532046851 +956,22,80,31500,40000,52.9918858786988 +957,22,80,31500,40500,72.36913743145999 +958,22,80,31500,41000,110.38003828747726 +959,22,80,31500,41500,157.65470091455973 +960,22,120,29000,38000,1186.823326813257 +961,22,120,29000,38500,844.3317816964005 +962,22,120,29000,39000,567.7367986440256 +963,22,120,29000,39500,371.79782508970567 +964,22,120,29000,40000,256.9261857702517 +965,22,120,29000,40500,211.85466060592006 +966,22,120,29000,41000,220.09534855737033 +967,22,120,29000,41500,265.02731793490034 +968,22,120,29500,38000,1128.4568915685559 +969,22,120,29500,38500,787.7709648712951 +970,22,120,29500,39000,508.4832626962424 +971,22,120,29500,39500,308.52654841064975 +972,22,120,29500,40000,190.01030358402707 +973,22,120,29500,40500,141.62663282114926 +974,22,120,29500,41000,146.40704203984612 +975,22,120,29500,41500,187.48734389188584 +976,22,120,30000,38000,1094.7007205604846 +977,22,120,30000,38500,757.7313528729464 +978,22,120,30000,39000,471.282561364766 +979,22,120,30000,39500,262.0412520036699 +980,22,120,30000,40000,136.26956239282435 +981,22,120,30000,40500,82.4268827471484 +982,22,120,30000,41000,82.3695177584498 +983,22,120,30000,41500,118.51210034475737 +984,22,120,30500,38000,1111.0872182758205 +985,22,120,30500,38500,787.2204655558988 +986,22,120,30500,39000,481.85960605002055 +987,22,120,30500,39500,250.28740868446397 +988,22,120,30500,40000,109.21968920710272 +989,22,120,30500,40500,45.51600269221681 +990,22,120,30500,41000,38.172157811051115 +991,22,120,30500,41500,67.73748641348168 +992,22,120,31000,38000,1193.3958874354898 +993,22,120,31000,38500,923.0731791194576 +994,22,120,31000,39000,573.4457650536078 +995,22,120,31000,39500,294.2980811757103 +996,22,120,31000,40000,124.86249624679849 +997,22,120,31000,40500,43.948524347749846 +998,22,120,31000,41000,25.582084045731808 +999,22,120,31000,41500,46.36268252714472 +1000,22,120,31500,38000,1336.0993444856913 +1001,22,120,31500,38500,1194.893001664831 +1002,22,120,31500,39000,740.6584250286721 +1003,22,120,31500,39500,397.18127104230757 +1004,22,120,31500,40000,194.20390582893873 +1005,22,120,31500,40500,88.22588964369922 +1006,22,120,31500,41000,54.97797247760634 +1007,22,120,31500,41500,64.88195101638016 diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py new file mode 100644 index 00000000000..ff1287811cf --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py @@ -0,0 +1,57 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +The following script can be used to run semibatch parameter estimation in +parallel and save results to files for later analysis and graphics. +Example command: mpiexec -n 4 python parallel_example.py +""" +import numpy as np +import pandas as pd +from itertools import product +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model + + +def main(): + # Vars to estimate + theta_names = ['k1', 'k2', 'E1', 'E2'] + + # Data, list of json file names + data = [] + file_dirname = dirname(abspath(str(__file__))) + for exp_num in range(10): + file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) + data.append(file_name) + + # Note, the model already includes a 'SecondStageCost' expression + # for sum of squared error that will be used in parameter estimation + + pest = parmest.Estimator(generate_model, data, theta_names) + + ### Parameter estimation with bootstrap resampling + bootstrap_theta = pest.theta_est_bootstrap(100) + bootstrap_theta.to_csv('bootstrap_theta.csv') + + ### Compute objective at theta for likelihood ratio test + k1 = np.arange(4, 24, 3) + k2 = np.arange(40, 160, 40) + E1 = np.arange(29000, 32000, 500) + E2 = np.arange(38000, 42000, 500) + theta_vals = pd.DataFrame(list(product(k1, k2, E1, E2)), columns=theta_names) + + obj_at_theta = pest.objective_at_theta(theta_vals) + obj_at_theta.to_csv('obj_at_theta.csv') + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py new file mode 100644 index 00000000000..fc4c9f5c675 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py @@ -0,0 +1,42 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import json +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model + + +def main(): + # Vars to estimate + theta_names = ['k1', 'k2', 'E1', 'E2'] + + # Data, list of dictionaries + data = [] + file_dirname = dirname(abspath(str(__file__))) + for exp_num in range(10): + file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) + with open(file_name, 'r') as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for sum of squared error that will be used in parameter estimation + + pest = parmest.Estimator(generate_model, data, theta_names) + + obj, theta = pest.theta_est() + print(obj) + print(theta) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py new file mode 100644 index 00000000000..071e53236c4 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import json +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model +import pyomo.contrib.parmest.scenariocreator as sc + + +def main(): + # Vars to estimate in parmest + theta_names = ['k1', 'k2', 'E1', 'E2'] + + # Data: list of dictionaries + data = [] + file_dirname = dirname(abspath(str(__file__))) + for exp_num in range(10): + fname = join(file_dirname, 'exp' + str(exp_num + 1) + '.out') + with open(fname, 'r') as infile: + d = json.load(infile) + data.append(d) + + pest = parmest.Estimator(generate_model, data, theta_names) + + scenmaker = sc.ScenarioCreator(pest, "ipopt") + + # Make one scenario per experiment and write to a csv file + output_file = "scenarios.csv" + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv(output_file) + + # Use the bootstrap to make 3 scenarios and print + bootscens = sc.ScenarioSet("Bootstrap") + scenmaker.ScenariosFromBootstrap(bootscens, 3) + for s in bootscens.ScensIterator(): + print("{}, {}".format(s.name, s.probability)) + for n, v in s.ThetaVals.items(): + print(" {}={}".format(n, v)) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv new file mode 100644 index 00000000000..22f9a651bc3 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv @@ -0,0 +1,11 @@ +Name,Probability,k1,k2,E1,E2 +ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 +ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 +ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 +ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 +ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 +ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 +ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 +ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py new file mode 100644 index 00000000000..6762531a338 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py @@ -0,0 +1,287 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +Semibatch model, based on Nicholson et al. (2018). pyomo.dae: A modeling and +automatic discretization framework for optimization with di +erential and +algebraic equations. Mathematical Programming Computation, 10(2), 187-223. +""" +import json +from os.path import join, abspath, dirname +from pyomo.environ import ( + ConcreteModel, + Set, + Param, + Var, + Constraint, + ConstraintList, + Expression, + Objective, + TransformationFactory, + SolverFactory, + exp, + minimize, +) +from pyomo.dae import ContinuousSet, DerivativeVar + + +def generate_model(data): + # if data is a file name, then load file first + if isinstance(data, str): + file_name = data + try: + with open(file_name, "r") as infile: + data = json.load(infile) + except: + raise RuntimeError(f"Could not read {file_name} as json") + + # unpack and fix the data + cameastemp = data["Ca_meas"] + cbmeastemp = data["Cb_meas"] + ccmeastemp = data["Cc_meas"] + trmeastemp = data["Tr_meas"] + + cameas = {} + cbmeas = {} + ccmeas = {} + trmeas = {} + for i in cameastemp.keys(): + cameas[float(i)] = cameastemp[i] + cbmeas[float(i)] = cbmeastemp[i] + ccmeas[float(i)] = ccmeastemp[i] + trmeas[float(i)] = trmeastemp[i] + + m = ConcreteModel() + + # + # Measurement Data + # + m.measT = Set(initialize=sorted(cameas.keys())) + m.Ca_meas = Param(m.measT, initialize=cameas) + m.Cb_meas = Param(m.measT, initialize=cbmeas) + m.Cc_meas = Param(m.measT, initialize=ccmeas) + m.Tr_meas = Param(m.measT, initialize=trmeas) + + # + # Parameters for semi-batch reactor model + # + m.R = Param(initialize=8.314) # kJ/kmol/K + m.Mwa = Param(initialize=50.0) # kg/kmol + m.rhor = Param(initialize=1000.0) # kg/m^3 + m.cpr = Param(initialize=3.9) # kJ/kg/K + m.Tf = Param(initialize=300) # K + m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol + m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol + m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K + m.alphac = Param(initialize=0.7) # kJ/s/m^2/K + m.Aj = Param(initialize=5.0) # m^2 + m.Ac = Param(initialize=3.0) # m^2 + m.Vj = Param(initialize=0.9) # m^3 + m.Vc = Param(initialize=0.07) # m^3 + m.rhow = Param(initialize=700.0) # kg/m^3 + m.cpw = Param(initialize=3.1) # kJ/kg/K + m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) + m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) + m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) + m.Tr0 = Param(initialize=300.0) # K + m.Vr0 = Param(initialize=1.0) # m^3 + + m.time = ContinuousSet(bounds=(0, 21600), initialize=m.measT) # Time in seconds + + # + # Control Inputs + # + def _initTc(m, t): + if t < 10800: + return data["Tc1"] + else: + return data["Tc2"] + + m.Tc = Param( + m.time, initialize=_initTc, default=_initTc + ) # bounds= (288,432) Cooling coil temp, control input + + def _initFa(m, t): + if t < 10800: + return data["Fa1"] + else: + return data["Fa2"] + + m.Fa = Param( + m.time, initialize=_initFa, default=_initFa + ) # bounds=(0,0.05) Inlet flow rate, control input + + # + # Parameters being estimated + # + m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 + m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 + m.E1 = Var(initialize=27000.0, bounds=(25000, 40000)) # kJ/kmol Actual: 30000 + m.E2 = Var(initialize=45000.0, bounds=(35000, 50000)) # kJ/kmol Actual: 40000 + # m.E1.fix(30000) + # m.E2.fix(40000) + + # + # Time dependent variables + # + m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) + m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) + m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) + m.Vr = Var(m.time, initialize=m.Vr0) + m.Tr = Var(m.time, initialize=m.Tr0) + m.Tj = Var( + m.time, initialize=310.0, bounds=(288, None) + ) # Cooling jacket temp, follows coil temp until failure + + # + # Derivatives in the model + # + m.dCa = DerivativeVar(m.Ca) + m.dCb = DerivativeVar(m.Cb) + m.dCc = DerivativeVar(m.Cc) + m.dVr = DerivativeVar(m.Vr) + m.dTr = DerivativeVar(m.Tr) + + # + # Differential Equations in the model + # + + def _dCacon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCa[t] + == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + ) + + m.dCacon = Constraint(m.time, rule=_dCacon) + + def _dCbcon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCb[t] + == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + ) + + m.dCbcon = Constraint(m.time, rule=_dCbcon) + + def _dCccon(m, t): + if t == 0: + return Constraint.Skip + return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + + m.dCccon = Constraint(m.time, rule=_dCccon) + + def _dVrcon(m, t): + if t == 0: + return Constraint.Skip + return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor + + m.dVrcon = Constraint(m.time, rule=_dVrcon) + + def _dTrcon(m, t): + if t == 0: + return Constraint.Skip + return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[t] * ( + m.Tf - m.Tr[t] + ) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] * m.deltaH1 - m.k2 * exp( + -m.E2 / (m.R * m.Tr[t]) + ) * m.Cb[ + t + ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( + m.Tj[t] - m.Tr[t] + ) + m.alphac * m.Ac / m.Vr0 * ( + m.Tc[t] - m.Tr[t] + ) + + m.dTrcon = Constraint(m.time, rule=_dTrcon) + + def _singlecooling(m, t): + return m.Tc[t] == m.Tj[t] + + m.singlecooling = Constraint(m.time, rule=_singlecooling) + + # Initial Conditions + def _initcon(m): + yield m.Ca[m.time.first()] == m.Ca0 + yield m.Cb[m.time.first()] == m.Cb0 + yield m.Cc[m.time.first()] == m.Cc0 + yield m.Vr[m.time.first()] == m.Vr0 + yield m.Tr[m.time.first()] == m.Tr0 + + m.initcon = ConstraintList(rule=_initcon) + + # + # Stage-specific cost computations + # + def ComputeFirstStageCost_rule(model): + return 0 + + m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + def MissingMeasurements(m): + if data["experiment"] == 1: + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + elif data["experiment"] == 2: + return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) + else: + return sum( + (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + m.SecondStageCost = Expression(rule=MissingMeasurements) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + # Discretize model + disc = TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=4) + return m + + +def main(): + # Data loaded from files + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "exp2.out")) + with open(file_name, "r") as infile: + data = json.load(infile) + data["experiment"] = 2 + + model = generate_model(data) + solver = SolverFactory("ipopt") + solver.solve(model) + print("k1 = ", model.k1()) + print("E1 = ", model.E1()) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index cbdc9179f35..dc747217b31 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -63,6 +63,8 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet +import pyomo.contrib.parmest.parmest_deprecated as parmest_deprecated + parmest_available = numpy_available & pandas_available & scipy_available inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( @@ -336,16 +338,32 @@ class Estimator(object): Provides options to the solver (also the name of an attribute) """ - def __init__( - self, - model_function, - data, - theta_names, - obj_function=None, - tee=False, - diagnostic_mode=False, - solver_options=None, - ): + # backwards compatible constructor will accept the old inputs + # from parmest_deprecated as well as the new inputs using experiment lists + def __init__(self, *args, **kwargs): + + # use deprecated interface + self.pest_deprecated = None + if len(args) > 1: + logger.warning('Using deprecated parmest inputs (model_function, ' + + 'data, theta_names), please use experiment lists instead.') + self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) + return + + print("New parmest interface using Experiment lists coming soon!") + exit() + + # def __init__( + # self, + # model_function, + # data, + # theta_names, + # obj_function=None, + # tee=False, + # diagnostic_mode=False, + # solver_options=None, + # ): + self.model_function = model_function assert isinstance( @@ -906,6 +924,15 @@ def theta_est( cov: pd.DataFrame Covariance matrix of the fitted parameters (only for solver='ef_ipopt') """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est( + solver=solver, + return_values=return_values, + calc_cov=calc_cov, + cov_n=cov_n) + assert isinstance(solver, str) assert isinstance(return_values, list) assert isinstance(calc_cov, bool) @@ -956,6 +983,16 @@ def theta_est_bootstrap( Theta values for each sample and (if return_samples = True) the sample numbers used in each estimation """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_bootstrap( + bootstrap_samples, + samplesize=samplesize, + replacement=replacement, + seed=seed, + return_samples=return_samples) + assert isinstance(bootstrap_samples, int) assert isinstance(samplesize, (type(None), int)) assert isinstance(replacement, bool) @@ -1011,6 +1048,15 @@ def theta_est_leaveNout( Theta values for each sample and (if return_samples = True) the sample numbers left out of each estimation """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_leaveNout( + lNo, + lNo_samples=lNo_samples, + seed=seed, + return_samples=return_samples) + assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) assert isinstance(seed, (type(None), int)) @@ -1084,6 +1130,16 @@ def leaveNout_bootstrap_test( indicates if the theta estimate is in (True) or out (False) of the alpha region for a given distribution (based on the bootstrap results) """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.leaveNout_bootstrap_test( + lNo, + lNo_samples, + bootstrap_samples, + distribution, alphas, + seed=seed) + assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) assert isinstance(bootstrap_samples, int) @@ -1144,6 +1200,13 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): Objective value for each theta (infeasible solutions are omitted). """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.objective_at_theta( + theta_values=theta_values, + initialize_parmest_model=initialize_parmest_model) + if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': pass # skip assertion if model has no fitted parameters else: @@ -1258,6 +1321,15 @@ def likelihood_ratio_test( thresholds: pd.Series If return_threshold = True, the thresholds are also returned. """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.likelihood_ratio_test( + obj_at_theta, + obj_value, + alphas, + return_thresholds=return_thresholds) + assert isinstance(obj_at_theta, pd.DataFrame) assert isinstance(obj_value, (int, float)) assert isinstance(alphas, list) @@ -1310,6 +1382,15 @@ def confidence_region_test( If test_theta_values is not None, returns test theta value along with True (inside) or False (outside) for each alpha """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.confidence_region_test( + theta_values, + distribution, + alphas, + test_theta_values=test_theta_values) + assert isinstance(theta_values, pd.DataFrame) assert distribution in ['Rect', 'MVN', 'KDE'] assert isinstance(alphas, list) diff --git a/pyomo/contrib/parmest/parmest_deprecated.py b/pyomo/contrib/parmest/parmest_deprecated.py new file mode 100644 index 00000000000..cbdc9179f35 --- /dev/null +++ b/pyomo/contrib/parmest/parmest_deprecated.py @@ -0,0 +1,1366 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +#### Using mpi-sppy instead of PySP; May 2020 +#### Adding option for "local" EF starting Sept 2020 +#### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 + +# TODO: move use_mpisppy to a Pyomo configuration option +# +# False implies always use the EF that is local to parmest +use_mpisppy = True # Use it if we can but use local if not. +if use_mpisppy: + try: + # MPI-SPPY has an unfortunate side effect of outputting + # "[ 0.00] Initializing mpi-sppy" when it is imported. This can + # cause things like doctests to fail. We will suppress that + # information here. + from pyomo.common.tee import capture_output + + with capture_output(): + import mpisppy.utils.sputils as sputils + except ImportError: + use_mpisppy = False # we can't use it +if use_mpisppy: + # These things should be outside the try block. + sputils.disable_tictoc_output() + import mpisppy.opt.ef as st + import mpisppy.scenario_tree as scenario_tree +else: + import pyomo.contrib.parmest.utils.create_ef as local_ef + import pyomo.contrib.parmest.utils.scenario_tree as scenario_tree + +import re +import importlib as im +import logging +import types +import json +from itertools import combinations + +from pyomo.common.dependencies import ( + attempt_import, + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, +) + +import pyomo.environ as pyo + +from pyomo.opt import SolverFactory +from pyomo.environ import Block, ComponentUID + +import pyomo.contrib.parmest.utils as utils +import pyomo.contrib.parmest.graphics as graphics +from pyomo.dae import ContinuousSet + +parmest_available = numpy_available & pandas_available & scipy_available + +inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( + 'pyomo.contrib.interior_point.inverse_reduced_hessian' +) + +logger = logging.getLogger(__name__) + + +def ef_nonants(ef): + # Wrapper to call someone's ef_nonants + # (the function being called is very short, but it might be changed) + if use_mpisppy: + return sputils.ef_nonants(ef) + else: + return local_ef.ef_nonants(ef) + + +def _experiment_instance_creation_callback( + scenario_name, node_names=None, cb_data=None +): + """ + This is going to be called by mpi-sppy or the local EF and it will call into + the user's model's callback. + + Parameters: + ----------- + scenario_name: `str` Scenario name should end with a number + node_names: `None` ( Not used here ) + cb_data : dict with ["callback"], ["BootList"], + ["theta_names"], ["cb_data"], etc. + "cb_data" is passed through to user's callback function + that is the "callback" value. + "BootList" is None or bootstrap experiment number list. + (called cb_data by mpisppy) + + + Returns: + -------- + instance: `ConcreteModel` + instantiated scenario + + Note: + ---- + There is flexibility both in how the function is passed and its signature. + """ + assert cb_data is not None + outer_cb_data = cb_data + scen_num_str = re.compile(r'(\d+)$').search(scenario_name).group(1) + scen_num = int(scen_num_str) + basename = scenario_name[: -len(scen_num_str)] # to reconstruct name + + CallbackFunction = outer_cb_data["callback"] + + if callable(CallbackFunction): + callback = CallbackFunction + else: + cb_name = CallbackFunction + + if "CallbackModule" not in outer_cb_data: + raise RuntimeError( + "Internal Error: need CallbackModule in parmest callback" + ) + else: + modname = outer_cb_data["CallbackModule"] + + if isinstance(modname, str): + cb_module = im.import_module(modname, package=None) + elif isinstance(modname, types.ModuleType): + cb_module = modname + else: + print("Internal Error: bad CallbackModule") + raise + + try: + callback = getattr(cb_module, cb_name) + except: + print("Error getting function=" + cb_name + " from module=" + str(modname)) + raise + + if "BootList" in outer_cb_data: + bootlist = outer_cb_data["BootList"] + # print("debug in callback: using bootlist=",str(bootlist)) + # assuming bootlist itself is zero based + exp_num = bootlist[scen_num] + else: + exp_num = scen_num + + scen_name = basename + str(exp_num) + + cb_data = outer_cb_data["cb_data"] # cb_data might be None. + + # at least three signatures are supported. The first is preferred + try: + instance = callback(experiment_number=exp_num, cb_data=cb_data) + except TypeError: + raise RuntimeError( + "Only one callback signature is supported: " + "callback(experiment_number, cb_data) " + ) + """ + try: + instance = callback(scenario_tree_model, scen_name, node_names) + except TypeError: # deprecated signature? + try: + instance = callback(scen_name, node_names) + except: + print("Failed to create instance using callback; TypeError+") + raise + except: + print("Failed to create instance using callback.") + raise + """ + if hasattr(instance, "_mpisppy_node_list"): + raise RuntimeError(f"scenario for experiment {exp_num} has _mpisppy_node_list") + nonant_list = [ + instance.find_component(vstr) for vstr in outer_cb_data["theta_names"] + ] + if use_mpisppy: + instance._mpisppy_node_list = [ + scenario_tree.ScenarioNode( + name="ROOT", + cond_prob=1.0, + stage=1, + cost_expression=instance.FirstStageCost, + nonant_list=nonant_list, + scen_model=instance, + ) + ] + else: + instance._mpisppy_node_list = [ + scenario_tree.ScenarioNode( + name="ROOT", + cond_prob=1.0, + stage=1, + cost_expression=instance.FirstStageCost, + scen_name_list=None, + nonant_list=nonant_list, + scen_model=instance, + ) + ] + + if "ThetaVals" in outer_cb_data: + thetavals = outer_cb_data["ThetaVals"] + + # dlw august 2018: see mea code for more general theta + for vstr in thetavals: + theta_cuid = ComponentUID(vstr) + theta_object = theta_cuid.find_component_on(instance) + if thetavals[vstr] is not None: + # print("Fixing",vstr,"at",str(thetavals[vstr])) + theta_object.fix(thetavals[vstr]) + else: + # print("Freeing",vstr) + theta_object.unfix() + + return instance + + +# ============================================= +def _treemaker(scenlist): + """ + Makes a scenario tree (avoids dependence on daps) + + Parameters + ---------- + scenlist (list of `int`): experiment (i.e. scenario) numbers + + Returns + ------- + a `ConcreteModel` that is the scenario tree + """ + + num_scenarios = len(scenlist) + m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() + m = m.create_instance() + m.Stages.add('Stage1') + m.Stages.add('Stage2') + m.Nodes.add('RootNode') + for i in scenlist: + m.Nodes.add('LeafNode_Experiment' + str(i)) + m.Scenarios.add('Experiment' + str(i)) + m.NodeStage['RootNode'] = 'Stage1' + m.ConditionalProbability['RootNode'] = 1.0 + for node in m.Nodes: + if node != 'RootNode': + m.NodeStage[node] = 'Stage2' + m.Children['RootNode'].add(node) + m.Children[node].clear() + m.ConditionalProbability[node] = 1.0 / num_scenarios + m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node + + return m + + +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + +class _SecondStageCostExpr(object): + """ + Class to pass objective expression into the Pyomo model + """ + + def __init__(self, ssc_function, data): + self._ssc_function = ssc_function + self._data = data + + def __call__(self, model): + return self._ssc_function(model, self._data) + + +class Estimator(object): + """ + Parameter estimation class + + Parameters + ---------- + model_function: function + Function that generates an instance of the Pyomo model using 'data' + as the input argument + data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names + Data that is used to build an instance of the Pyomo model and build + the objective function + theta_names: list of strings + List of Var names to estimate + obj_function: function, optional + Function used to formulate parameter estimation objective, generally + sum of squared error between measurements and model variables. + If no function is specified, the model is used + "as is" and should be defined with a "FirstStageCost" and + "SecondStageCost" expression that are used to build an objective. + tee: bool, optional + Indicates that ef solver output should be teed + diagnostic_mode: bool, optional + If True, print diagnostics from the solver + solver_options: dict, optional + Provides options to the solver (also the name of an attribute) + """ + + def __init__( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + self.model_function = model_function + + assert isinstance( + data, (list, pd.DataFrame) + ), "Data must be a list or DataFrame" + # convert dataframe into a list of dataframes, each row = one scenario + if isinstance(data, pd.DataFrame): + self.callback_data = [ + data.loc[i, :].to_frame().transpose() for i in data.index + ] + else: + self.callback_data = data + assert isinstance( + self.callback_data[0], (dict, pd.DataFrame, str) + ), "The scenarios in data must be a dictionary, DataFrame or filename" + + if len(theta_names) == 0: + self.theta_names = ['parmest_dummy_var'] + else: + self.theta_names = theta_names + + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options + + self._second_stage_cost_exp = "SecondStageCost" + # boolean to indicate if model is initialized using a square solve + self.model_initialized = False + + def _return_theta_names(self): + """ + Return list of fitted model parameter names + """ + # if fitted model parameter names differ from theta_names created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, data): + """ + Modify the Pyomo model for parameter estimation + """ + model = self.model_function(data) + + if (len(self.theta_names) == 1) and ( + self.theta_names[0] == 'parmest_dummy_var' + ): + model.parmest_dummy_var = pyo.Var(initialize=1.0) + + # Add objective function (optional) + if self.obj_function: + for obj in model.component_objects(pyo.Objective): + if obj.name in ["Total_Cost_Objective"]: + raise RuntimeError( + "Parmest will not override the existing model Objective named " + + obj.name + ) + obj.deactivate() + + for expr in model.component_data_objects(pyo.Expression): + if expr.name in ["FirstStageCost", "SecondStageCost"]: + raise RuntimeError( + "Parmest will not override the existing model Expression named " + + expr.name + ) + model.FirstStageCost = pyo.Expression(expr=0) + model.SecondStageCost = pyo.Expression( + rule=_SecondStageCostExpr(self.obj_function, data) + ) + + def TotalCost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + model.Total_Cost_Objective = pyo.Objective( + rule=TotalCost_rule, sense=pyo.minimize + ) + + # Convert theta Params to Vars, and unfix theta Vars + model = utils.convert_params_to_vars(model, self.theta_names) + + # Update theta names list to use CUID string representation + for i, theta in enumerate(self.theta_names): + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(model) + if var_validate is None: + logger.warning( + "theta_name[%s] (%s) was not found on the model", (i, theta) + ) + else: + try: + # If the component is not a variable, + # this will generate an exception (and the warning + # in the 'except') + var_validate.unfix() + self.theta_names[i] = repr(var_cuid) + except: + logger.warning(theta + ' is not a variable') + + self.parmest_model = model + + return model + + def _instance_creation_callback(self, experiment_number=None, cb_data=None): + # cb_data is a list of dictionaries, list of dataframes, OR list of json file names + exp_data = cb_data[experiment_number] + if isinstance(exp_data, (dict, pd.DataFrame)): + pass + elif isinstance(exp_data, str): + try: + with open(exp_data, 'r') as infile: + exp_data = json.load(infile) + except: + raise RuntimeError(f'Could not read {exp_data} as json') + else: + raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') + model = self._create_parmest_model(exp_data) + + return model + + def _Q_opt( + self, + ThetaVals=None, + solver="ef_ipopt", + return_values=[], + bootlist=None, + calc_cov=False, + cov_n=None, + ): + """ + Set up all thetas as first stage Vars, return resulting theta + values as well as the objective function value. + + """ + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") + + # (Bootstrap scenarios will use indirection through the bootlist) + if bootlist is None: + scenario_numbers = list(range(len(self.callback_data))) + scen_names = ["Scenario{}".format(i) for i in scenario_numbers] + else: + scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] + + # tree_model.CallbackModule = None + outer_cb_data = dict() + outer_cb_data["callback"] = self._instance_creation_callback + if ThetaVals is not None: + outer_cb_data["ThetaVals"] = ThetaVals + if bootlist is not None: + outer_cb_data["BootList"] = bootlist + outer_cb_data["cb_data"] = self.callback_data # None is OK + outer_cb_data["theta_names"] = self.theta_names + + options = {"solver": "ipopt"} + scenario_creator_options = {"cb_data": outer_cb_data} + if use_mpisppy: + ef = sputils.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + else: + ef = local_ef.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + self.ef_instance = ef + + # Solve the extensive form with ipopt + if solver == "ef_ipopt": + if not calc_cov: + # Do not calculate the reduced hessian + + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] + + solve_result = solver.solve(self.ef_instance, tee=self.tee) + + # The import error will be raised when we attempt to use + # inv_reduced_hessian_barrier below. + # + # elif not asl_available: + # raise ImportError("parmest requires ASL to calculate the " + # "covariance matrix with solver 'ipopt'") + else: + # parmest makes the fitted parameters stage 1 variables + ind_vars = [] + for ndname, Var, solval in ef_nonants(ef): + ind_vars.append(Var) + # calculate the reduced hessian + ( + solve_result, + inv_red_hes, + ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + + if self.diagnostic_mode: + print( + ' Solver termination condition = ', + str(solve_result.solver.termination_condition), + ) + + # assume all first stage are thetas... + thetavals = {} + for ndname, Var, solval in ef_nonants(ef): + # process the name + # the scenarios are blocks, so strip the scenario name + vname = Var.name[Var.name.find(".") + 1 :] + thetavals[vname] = solval + + objval = pyo.value(ef.EF_Obj) + + if calc_cov: + # Calculate the covariance matrix + + # Number of data points considered + n = cov_n + + # Extract number of fitted parameters + l = len(thetavals) + + # Assumption: Objective value is sum of squared errors + sse = objval + + '''Calculate covariance assuming experimental observation errors are + independent and follow a Gaussian + distribution with constant variance. + + The formula used in parmest was verified against equations (7-5-15) and + (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. + + This formula is also applicable if the objective is scaled by a constant; + the constant cancels out. (was scaled by 1/n because it computes an + expected value.) + ''' + cov = 2 * sse / (n - l) * inv_red_hes + cov = pd.DataFrame( + cov, index=thetavals.keys(), columns=thetavals.keys() + ) + + thetavals = pd.Series(thetavals) + + if len(return_values) > 0: + var_values = [] + if len(scen_names) > 1: # multiple scenarios + block_objects = self.ef_instance.component_objects( + Block, descend_into=False + ) + else: # single scenario + block_objects = [self.ef_instance] + for exp_i in block_objects: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if ( + exp_i_var is None + ): # we might have a block such as _mpisppy_data + continue + # if value to return is ContinuousSet + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + if calc_cov: + return objval, thetavals, var_values, cov + else: + return objval, thetavals, var_values + + if calc_cov: + return objval, thetavals, cov + else: + return objval, thetavals + + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + + def _Q_at_theta(self, thetavals, initialize_parmest_model=False): + """ + Return the objective function value with fixed theta values. + + Parameters + ---------- + thetavals: dict + A dictionary of theta values. + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + Returns + ------- + objectiveval: float + The objective function value. + thetavals: dict + A dictionary of all values for theta that were input. + solvertermination: Pyomo TerminationCondition + Tries to return the "worst" solver status across the scenarios. + pyo.TerminationCondition.optimal is the best and + pyo.TerminationCondition.infeasible is the worst. + """ + + optimizer = pyo.SolverFactory('ipopt') + + if len(thetavals) > 0: + dummy_cb = { + "callback": self._instance_creation_callback, + "ThetaVals": thetavals, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + else: + dummy_cb = { + "callback": self._instance_creation_callback, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + + if self.diagnostic_mode: + if len(thetavals) > 0: + print(' Compute objective at theta = ', str(thetavals)) + else: + print(' Compute objective at initial theta') + + # start block of code to deal with models with no constraints + # (ipopt will crash or complain on such problems without special care) + instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) + try: # deal with special problems so Ipopt will not crash + first = next(instance.component_objects(pyo.Constraint, active=True)) + active_constraints = True + except: + active_constraints = False + # end block of code to deal with models with no constraints + + WorstStatus = pyo.TerminationCondition.optimal + totobj = 0 + scenario_numbers = list(range(len(self.callback_data))) + if initialize_parmest_model: + # create dictionary to store pyomo model instances (scenarios) + scen_dict = dict() + + for snum in scenario_numbers: + sname = "scenario_NODE" + str(snum) + instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + + if initialize_parmest_model: + # list to store fitted parameter names that will be unfixed + # after initialization + theta_init_vals = [] + # use appropriate theta_names member + theta_ref = self._return_theta_names() + + for i, theta in enumerate(theta_ref): + # Use parser in ComponentUID to locate the component + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(instance) + if var_validate is None: + logger.warning( + "theta_name %s was not found on the model", (theta) + ) + else: + try: + if len(thetavals) == 0: + var_validate.fix() + else: + var_validate.fix(thetavals[theta]) + theta_init_vals.append(var_validate) + except: + logger.warning( + 'Unable to fix model parameter value for %s (not a Pyomo model Var)', + (theta), + ) + + if active_constraints: + if self.diagnostic_mode: + print(' Experiment = ', snum) + print(' First solve with special diagnostics wrapper') + ( + status_obj, + solved, + iters, + time, + regu, + ) = utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) + print( + " status_obj, solved, iters, time, regularization_stat = ", + str(status_obj), + str(solved), + str(iters), + str(time), + str(regu), + ) + + results = optimizer.solve(instance) + if self.diagnostic_mode: + print( + 'standard solve solver termination condition=', + str(results.solver.termination_condition), + ) + + if ( + results.solver.termination_condition + != pyo.TerminationCondition.optimal + ): + # DLW: Aug2018: not distinguishing "middlish" conditions + if WorstStatus != pyo.TerminationCondition.infeasible: + WorstStatus = results.solver.termination_condition + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} infeasible with initialized parameter values".format( + snum + ) + ) + else: + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} initialization successful with initial parameter values".format( + snum + ) + ) + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + else: + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + + objobject = getattr(instance, self._second_stage_cost_exp) + objval = pyo.value(objobject) + totobj += objval + + retval = totobj / len(scenario_numbers) # -1?? + if initialize_parmest_model and not hasattr(self, 'ef_instance'): + # create extensive form of the model using scenario dictionary + if len(scen_dict) > 0: + for scen in scen_dict.values(): + scen._mpisppy_probability = 1 / len(scen_dict) + + if use_mpisppy: + EF_instance = sputils._create_EF_from_scen_dict( + scen_dict, + EF_name="_Q_at_theta", + # suppress_warnings=True + ) + else: + EF_instance = local_ef._create_EF_from_scen_dict( + scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True + ) + + self.ef_instance = EF_instance + # set self.model_initialized flag to True to skip extensive form model + # creation using theta_est() + self.model_initialized = True + + # return initialized theta values + if len(thetavals) == 0: + # use appropriate theta_names member + theta_ref = self._return_theta_names() + for i, theta in enumerate(theta_ref): + thetavals[theta] = theta_init_vals[i]() + + return retval, thetavals, WorstStatus + + def _get_sample_list(self, samplesize, num_samples, replacement=True): + samplelist = list() + + scenario_numbers = list(range(len(self.callback_data))) + + if num_samples is None: + # This could get very large + for i, l in enumerate(combinations(scenario_numbers, samplesize)): + samplelist.append((i, np.sort(l))) + else: + for i in range(num_samples): + attempts = 0 + unique_samples = 0 # check for duplicates in each sample + duplicate = False # check for duplicates between samples + while (unique_samples <= len(self._return_theta_names())) and ( + not duplicate + ): + sample = np.random.choice( + scenario_numbers, samplesize, replace=replacement + ) + sample = np.sort(sample).tolist() + unique_samples = len(np.unique(sample)) + if sample in samplelist: + duplicate = True + + attempts += 1 + if attempts > num_samples: # arbitrary timeout limit + raise RuntimeError( + """Internal error: timeout constructing + a sample, the dim of theta may be too + close to the samplesize""" + ) + + samplelist.append((i, sample)) + + return samplelist + + def theta_est( + self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: string, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model for data reconciliation + calc_cov: boolean, optional + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) + cov_n: int, optional + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function + + Returns + ------- + objectiveval: float + The objective function value + thetavals: pd.Series + Estimated values for theta + variable values: pd.DataFrame + Variable values for each variable name in return_values (only for solver='ef_ipopt') + cov: pd.DataFrame + Covariance matrix of the fitted parameters (only for solver='ef_ipopt') + """ + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert isinstance(calc_cov, bool) + if calc_cov: + assert isinstance( + cov_n, int + ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert cov_n > len( + self._return_theta_names() + ), "The number of datapoints must be greater than the number of parameters to estimate" + + return self._Q_opt( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + def theta_est_bootstrap( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.callback_data) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta + + def theta_est_leaveNout( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.callback_data) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + lNo_s = list(set(range(len(self.callback_data))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + + def leaveNout_bootstrap_test( + self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None + ): + """ + Leave-N-out bootstrap test to compare theta values where N data points are + left out to a bootstrap analysis using the remaining data, + results indicate if theta is within a confidence region + determined by the bootstrap analysis + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Leave-N-out sample size. If lNo_samples=None, the maximum number + of combinations will be used + bootstrap_samples: int: + Bootstrap sample size + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + seed: int or None, optional + Random seed + + Returns + ---------- + List of tuples with one entry per lNo_sample: + + * The first item in each tuple is the list of N samples that are left + out. + * The second item in each tuple is a DataFrame of theta estimated using + the N samples. + * The third item in each tuple is a DataFrame containing results from + the bootstrap analysis using the remaining samples. + + For each DataFrame a column is added for each value of alpha which + indicates if the theta estimate is in (True) or out (False) of the + alpha region for a given distribution (based on the bootstrap results) + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(bootstrap_samples, int) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance(seed, (type(None), int)) + + if seed is not None: + np.random.seed(seed) + + data = self.callback_data.copy() + + global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) + + results = [] + for idx, sample in global_list: + # Reset callback_data to only include the sample + self.callback_data = [data[i] for i in sample] + + obj, theta = self.theta_est() + + # Reset callback_data to include all scenarios except the sample + self.callback_data = [data[i] for i in range(len(data)) if i not in sample] + + bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) + + training, test = self.confidence_region_test( + bootstrap_theta, + distribution=distribution, + alphas=alphas, + test_theta_values=theta, + ) + + results.append((sample, test, training)) + + # Reset callback_data (back to full data set) + self.callback_data = data + + return results + + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): + """ + Objective value for each theta + + Parameters + ---------- + theta_values: pd.DataFrame, columns=theta_names + Values of theta used to compute the objective + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + + Returns + ------- + obj_at_theta: pd.DataFrame + Objective value for each theta (infeasible solutions are + omitted). + """ + if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': + pass # skip assertion if model has no fitted parameters + else: + # create a local instance of the pyomo model to access model variables and parameters + model_temp = self._create_parmest_model(self.callback_data[0]) + model_theta_list = [] # list to store indexed and non-indexed parameters + # iterate over original theta_names + for theta_i in self.theta_names: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + # check if theta in theta_names are indexed + try: + # get component UID of Set over which theta is defined + set_cuid = ComponentUID(var_validate.index_set()) + # access and iterate over the Set to generate theta names as they appear + # in the pyomo model + set_validate = set_cuid.find_component_on(model_temp) + for s in set_validate: + self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" + # generate list of theta names + model_theta_list.append(self_theta_temp) + # if theta is not indexed, copy theta name to list as-is + except AttributeError: + self_theta_temp = repr(var_cuid) + model_theta_list.append(self_theta_temp) + except: + raise + # if self.theta_names is not the same as temp model_theta_list, + # create self.theta_names_updated + if set(self.theta_names) == set(model_theta_list) and len( + self.theta_names + ) == set(model_theta_list): + pass + else: + self.theta_names_updated = model_theta_list + + if theta_values is None: + all_thetas = {} # dictionary to store fitted variables + # use appropriate theta names member + theta_names = self._return_theta_names() + else: + assert isinstance(theta_values, pd.DataFrame) + # for parallel code we need to use lists and dicts in the loop + theta_names = theta_values.columns + # # check if theta_names are in model + for theta in list(theta_names): + theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + + assert theta_temp in [ + t.replace("'", "") for t in model_theta_list + ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + theta_temp, model_theta_list + ) + assert len(list(theta_names)) == len(model_theta_list) + + all_thetas = theta_values.to_dict('records') + + if all_thetas: + task_mgr = utils.ParallelTaskManager(len(all_thetas)) + local_thetas = task_mgr.global_to_local_data(all_thetas) + else: + if initialize_parmest_model: + task_mgr = utils.ParallelTaskManager( + 1 + ) # initialization performed using just 1 set of theta values + # walk over the mesh, return objective function + all_obj = list() + if len(all_thetas) > 0: + for Theta in local_thetas: + obj, thetvals, worststatus = self._Q_at_theta( + Theta, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(Theta.values()) + [obj]) + # DLW, Aug2018: should we also store the worst solver status? + else: + obj, thetvals, worststatus = self._Q_at_theta( + thetavals={}, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(thetvals.values()) + [obj]) + + global_all_obj = task_mgr.allgather_global_data(all_obj) + dfcols = list(theta_names) + ['obj'] + obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + return obj_at_theta + + def likelihood_ratio_test( + self, obj_at_theta, obj_value, alphas, return_thresholds=False + ): + r""" + Likelihood ratio test to identify theta values within a confidence + region using the :math:`\chi^2` distribution + + Parameters + ---------- + obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' + Objective values for each theta value (returned by + objective_at_theta) + obj_value: int or float + Objective value from parameter estimation using all data + alphas: list + List of alpha values to use in the chi2 test + return_thresholds: bool, optional + Return the threshold value for each alpha + + Returns + ------- + LR: pd.DataFrame + Objective values for each theta value along with True or False for + each alpha + thresholds: pd.Series + If return_threshold = True, the thresholds are also returned. + """ + assert isinstance(obj_at_theta, pd.DataFrame) + assert isinstance(obj_value, (int, float)) + assert isinstance(alphas, list) + assert isinstance(return_thresholds, bool) + + LR = obj_at_theta.copy() + S = len(self.callback_data) + thresholds = {} + for a in alphas: + chi2_val = scipy.stats.chi2.ppf(a, 2) + thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) + LR[a] = LR['obj'] < thresholds[a] + + thresholds = pd.Series(thresholds) + + if return_thresholds: + return LR, thresholds + else: + return LR + + def confidence_region_test( + self, theta_values, distribution, alphas, test_theta_values=None + ): + """ + Confidence region test to determine if theta values are within a + rectangular, multivariate normal, or Gaussian kernel density distribution + for a range of alpha values + + Parameters + ---------- + theta_values: pd.DataFrame, columns = theta_names + Theta values used to generate a confidence region + (generally returned by theta_est_bootstrap) + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional + Additional theta values that are compared to the confidence region + to determine if they are inside or outside. + + Returns + training_results: pd.DataFrame + Theta value used to generate the confidence region along with True + (inside) or False (outside) for each alpha + test_results: pd.DataFrame + If test_theta_values is not None, returns test theta value along + with True (inside) or False (outside) for each alpha + """ + assert isinstance(theta_values, pd.DataFrame) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance( + test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) + ) + + if isinstance(test_theta_values, (dict, pd.Series)): + test_theta_values = pd.Series(test_theta_values).to_frame().transpose() + + training_results = theta_values.copy() + + if test_theta_values is not None: + test_result = test_theta_values.copy() + + for a in alphas: + if distribution == 'Rect': + lb, ub = graphics.fit_rect_dist(theta_values, a) + training_results[a] = (theta_values > lb).all(axis=1) & ( + theta_values < ub + ).all(axis=1) + + if test_theta_values is not None: + # use upper and lower bound from the training set + test_result[a] = (test_theta_values > lb).all(axis=1) & ( + test_theta_values < ub + ).all(axis=1) + + elif distribution == 'MVN': + dist = graphics.fit_mvn_dist(theta_values) + Z = dist.pdf(theta_values) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values) + test_result[a] = Z >= score + + elif distribution == 'KDE': + dist = graphics.fit_kde_dist(theta_values) + Z = dist.pdf(theta_values.transpose()) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values.transpose()) + test_result[a] = Z >= score + + if test_theta_values is not None: + return training_results, test_result + else: + return training_results diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 58d2d4da722..18c27ad1c86 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,6 +14,10 @@ import pyomo.environ as pyo +import pyomo.contrib.parmest.scenariocreator_deprecated as scen_deprecated + +import logging +logger = logging.getLogger(__name__) class ScenarioSet(object): """ @@ -119,8 +123,17 @@ class ScenarioCreator(object): """ def __init__(self, pest, solvername): - self.pest = pest - self.solvername = solvername + + # is this a deprecated pest object? + self.scen_deprecated = None + if pest.pest_deprecated is not None: + logger.warning("Using a deprecated parmest object for scenario " + + "creator, please recreate object using experiment lists.") + self.scen_deprecated = scen_deprecated.ScenarioCreator( + pest.pest_deprecated, solvername) + else: + self.pest = pest + self.solvername = solvername def ScenariosFromExperiments(self, addtoSet): """Creates new self.Scenarios list using the experiments only. @@ -131,6 +144,11 @@ def ScenariosFromExperiments(self, addtoSet): a ScenarioSet """ + # check if using deprecated pest object + if self.scen_deprecated is not None: + self.scen_deprecated.ScenariosFromExperiments(addtoSet) + return + assert isinstance(addtoSet, ScenarioSet) scenario_numbers = list(range(len(self.pest.callback_data))) @@ -160,6 +178,12 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): numtomake (int) : number of scenarios to create """ + # check if using deprecated pest object + if self.scen_deprecated is not None: + self.scen_deprecated.ScenariosFromBootstrap( + addtoSet, numtomake, seed=seed) + return + assert isinstance(addtoSet, ScenarioSet) bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) diff --git a/pyomo/contrib/parmest/scenariocreator_deprecated.py b/pyomo/contrib/parmest/scenariocreator_deprecated.py new file mode 100644 index 00000000000..af084d0712c --- /dev/null +++ b/pyomo/contrib/parmest/scenariocreator_deprecated.py @@ -0,0 +1,166 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# ScenariosCreator.py - Class to create and deliver scenarios using parmest +# DLW March 2020 + +import pyomo.environ as pyo + + +class ScenarioSet(object): + """ + Class to hold scenario sets + + Args: + name (str): name of the set (might be "") + + """ + + def __init__(self, name): + # Note: If there was a use-case, the list could be a dataframe. + self._scens = list() # use a df instead? + self.name = name # might be "" + + def _firstscen(self): + # Return the first scenario for testing and to get Theta names. + assert len(self._scens) > 0 + return self._scens[0] + + def ScensIterator(self): + """Usage: for scenario in ScensIterator()""" + return iter(self._scens) + + def ScenarioNumber(self, scennum): + """Returns the scenario with the given, zero-based number""" + return self._scens[scennum] + + def addone(self, scen): + """Add a scenario to the set + + Args: + scen (ParmestScen): the scenario to add + """ + assert isinstance(self._scens, list) + self._scens.append(scen) + + def append_bootstrap(self, bootstrap_theta): + """Append a bootstrap theta df to the scenario set; equally likely + + Args: + bootstrap_theta (dataframe): created by the bootstrap + Note: this can be cleaned up a lot with the list becomes a df, + which is why I put it in the ScenarioSet class. + """ + assert len(bootstrap_theta) > 0 + prob = 1.0 / len(bootstrap_theta) + + # dict of ThetaVal dicts + dfdict = bootstrap_theta.to_dict(orient='index') + + for index, ThetaVals in dfdict.items(): + name = "Bootstrap" + str(index) + self.addone(ParmestScen(name, ThetaVals, prob)) + + def write_csv(self, filename): + """write a csv file with the scenarios in the set + + Args: + filename (str): full path and full name of file + """ + if len(self._scens) == 0: + print("Empty scenario set, not writing file={}".format(filename)) + return + with open(filename, "w") as f: + f.write("Name,Probability") + for n in self._firstscen().ThetaVals.keys(): + f.write(",{}".format(n)) + f.write('\n') + for s in self.ScensIterator(): + f.write("{},{}".format(s.name, s.probability)) + for v in s.ThetaVals.values(): + f.write(",{}".format(v)) + f.write('\n') + + +class ParmestScen(object): + """A little container for scenarios; the Args are the attributes. + + Args: + name (str): name for reporting; might be "" + ThetaVals (dict): ThetaVals[name]=val + probability (float): probability of occurrence "near" these ThetaVals + """ + + def __init__(self, name, ThetaVals, probability): + self.name = name + assert isinstance(ThetaVals, dict) + self.ThetaVals = ThetaVals + self.probability = probability + + +############################################################ + + +class ScenarioCreator(object): + """Create scenarios from parmest. + + Args: + pest (Estimator): the parmest object + solvername (str): name of the solver (e.g. "ipopt") + + """ + + def __init__(self, pest, solvername): + self.pest = pest + self.solvername = solvername + + def ScenariosFromExperiments(self, addtoSet): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + Returns: + a ScenarioSet + """ + + # assert isinstance(addtoSet, ScenarioSet) + + scenario_numbers = list(range(len(self.pest.callback_data))) + + prob = 1.0 / len(scenario_numbers) + for exp_num in scenario_numbers: + ##print("Experiment number=", exp_num) + model = self.pest._instance_creation_callback( + exp_num, self.pest.callback_data + ) + opt = pyo.SolverFactory(self.solvername) + results = opt.solve(model) # solves and updates model + ## pyo.check_termination_optimal(results) + ThetaVals = dict() + for theta in self.pest.theta_names: + tvar = eval('model.' + theta) + tval = pyo.value(tvar) + ##print(" theta, tval=", tvar, tval) + ThetaVals[theta] = tval + addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) + + def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + numtomake (int) : number of scenarios to create + """ + + # assert isinstance(addtoSet, ScenarioSet) + + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + addtoSet.append_bootstrap(bootstrap_thetas) From 87aa19df3deebbb0bf987fa6628d6acb3f86a24c Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 9 Jan 2024 14:00:25 -0700 Subject: [PATCH 0178/1178] Moved parmest deprecated files to folder. --- .../examples}/__init__.py | 0 .../examples}/reaction_kinetics/__init__.py | 0 .../simple_reaction_parmest_example.py | 0 .../examples}/reactor_design/__init__.py | 0 .../reactor_design/bootstrap_example.py | 0 .../reactor_design/datarec_example.py | 0 .../reactor_design/leaveNout_example.py | 0 .../likelihood_ratio_example.py | 0 .../multisensor_data_example.py | 0 .../parameter_estimation_example.py | 0 .../examples}/reactor_design/reactor_data.csv | 0 .../reactor_data_multisensor.csv | 0 .../reactor_data_timeseries.csv | 0 .../reactor_design/reactor_design.py | 0 .../reactor_design/timeseries_data_example.py | 0 .../examples}/rooney_biegler/__init__.py | 0 .../rooney_biegler/bootstrap_example.py | 0 .../likelihood_ratio_example.py | 0 .../parameter_estimation_example.py | 0 .../rooney_biegler/rooney_biegler.py | 0 .../rooney_biegler_with_constraint.py | 0 .../examples}/semibatch/__init__.py | 0 .../examples}/semibatch/bootstrap_theta.csv | 0 .../examples}/semibatch/obj_at_theta.csv | 0 .../examples}/semibatch/parallel_example.py | 0 .../semibatch/parameter_estimation_example.py | 0 .../examples}/semibatch/scenario_example.py | 0 .../examples}/semibatch/scenarios.csv | 0 .../examples}/semibatch/semibatch.py | 0 .../parmest.py} | 0 .../scenariocreator.py} | 0 .../parmest/deprecated/tests/__init__.py | 10 + .../parmest/deprecated/tests/scenarios.csv | 11 + .../parmest/deprecated/tests/test_examples.py | 192 ++++ .../parmest/deprecated/tests/test_graphics.py | 68 ++ .../parmest/deprecated/tests/test_parmest.py | 958 ++++++++++++++++++ .../deprecated/tests/test_scenariocreator.py | 146 +++ .../parmest/deprecated/tests/test_solver.py | 75 ++ .../parmest/deprecated/tests/test_utils.py | 68 ++ pyomo/contrib/parmest/parmest.py | 21 +- pyomo/contrib/parmest/scenariocreator.py | 2 +- 41 files changed, 1543 insertions(+), 8 deletions(-) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reaction_kinetics/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reaction_kinetics/simple_reaction_parmest_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/bootstrap_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/datarec_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/leaveNout_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/likelihood_ratio_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/multisensor_data_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/parameter_estimation_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_data.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_data_multisensor.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_data_timeseries.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_design.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/timeseries_data_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/bootstrap_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/likelihood_ratio_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/parameter_estimation_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/rooney_biegler.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/rooney_biegler_with_constraint.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/bootstrap_theta.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/obj_at_theta.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/parallel_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/parameter_estimation_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/scenario_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/scenarios.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/semibatch.py (100%) rename pyomo/contrib/parmest/{parmest_deprecated.py => deprecated/parmest.py} (100%) rename pyomo/contrib/parmest/{scenariocreator_deprecated.py => deprecated/scenariocreator.py} (100%) create mode 100644 pyomo/contrib/parmest/deprecated/tests/__init__.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/scenarios.csv create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_examples.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_graphics.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_parmest.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_solver.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_utils.py diff --git a/pyomo/contrib/parmest/examples_deprecated/__init__.py b/pyomo/contrib/parmest/deprecated/examples/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py rename to pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv rename to pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv rename to pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv rename to pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py diff --git a/pyomo/contrib/parmest/parmest_deprecated.py b/pyomo/contrib/parmest/deprecated/parmest.py similarity index 100% rename from pyomo/contrib/parmest/parmest_deprecated.py rename to pyomo/contrib/parmest/deprecated/parmest.py diff --git a/pyomo/contrib/parmest/scenariocreator_deprecated.py b/pyomo/contrib/parmest/deprecated/scenariocreator.py similarity index 100% rename from pyomo/contrib/parmest/scenariocreator_deprecated.py rename to pyomo/contrib/parmest/deprecated/scenariocreator.py diff --git a/pyomo/contrib/parmest/deprecated/tests/__init__.py b/pyomo/contrib/parmest/deprecated/tests/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/tests/scenarios.csv b/pyomo/contrib/parmest/deprecated/tests/scenarios.csv new file mode 100644 index 00000000000..22f9a651bc3 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/scenarios.csv @@ -0,0 +1,11 @@ +Name,Probability,k1,k2,E1,E2 +ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 +ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 +ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 +ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 +ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 +ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 +ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 +ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py new file mode 100644 index 00000000000..67e06130384 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_examples.py @@ -0,0 +1,192 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.graphics import matplotlib_available, seaborn_available +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestRooneyBieglerExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_model(self): + from pyomo.contrib.parmest.examples.rooney_biegler import rooney_biegler + + rooney_biegler.main() + + def test_model_with_constraint(self): + from pyomo.contrib.parmest.examples.rooney_biegler import ( + rooney_biegler_with_constraint, + ) + + rooney_biegler_with_constraint.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_parameter_estimation_example(self): + from pyomo.contrib.parmest.examples.rooney_biegler import ( + parameter_estimation_example, + ) + + parameter_estimation_example.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_bootstrap_example(self): + from pyomo.contrib.parmest.examples.rooney_biegler import bootstrap_example + + bootstrap_example.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_likelihood_ratio_example(self): + from pyomo.contrib.parmest.examples.rooney_biegler import ( + likelihood_ratio_example, + ) + + likelihood_ratio_example.main() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactionKineticsExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_example(self): + from pyomo.contrib.parmest.examples.reaction_kinetics import ( + simple_reaction_parmest_example, + ) + + simple_reaction_parmest_example.main() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestSemibatchExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_model(self): + from pyomo.contrib.parmest.examples.semibatch import semibatch + + semibatch.main() + + def test_parameter_estimation_example(self): + from pyomo.contrib.parmest.examples.semibatch import ( + parameter_estimation_example, + ) + + parameter_estimation_example.main() + + def test_scenario_example(self): + from pyomo.contrib.parmest.examples.semibatch import scenario_example + + scenario_example.main() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesignExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + @unittest.pytest.mark.expensive + def test_model(self): + from pyomo.contrib.parmest.examples.reactor_design import reactor_design + + reactor_design.main() + + def test_parameter_estimation_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + parameter_estimation_example, + ) + + parameter_estimation_example.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_bootstrap_example(self): + from pyomo.contrib.parmest.examples.reactor_design import bootstrap_example + + bootstrap_example.main() + + @unittest.pytest.mark.expensive + def test_likelihood_ratio_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + likelihood_ratio_example, + ) + + likelihood_ratio_example.main() + + @unittest.pytest.mark.expensive + def test_leaveNout_example(self): + from pyomo.contrib.parmest.examples.reactor_design import leaveNout_example + + leaveNout_example.main() + + def test_timeseries_data_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + timeseries_data_example, + ) + + timeseries_data_example.main() + + def test_multisensor_data_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + multisensor_data_example, + ) + + multisensor_data_example.main() + + @unittest.skipUnless(matplotlib_available, "test requires matplotlib") + def test_datarec_example(self): + from pyomo.contrib.parmest.examples.reactor_design import datarec_example + + datarec_example.main() + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_graphics.py b/pyomo/contrib/parmest/deprecated/tests/test_graphics.py new file mode 100644 index 00000000000..c18659e9948 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_graphics.py @@ -0,0 +1,68 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, + matplotlib, + matplotlib_available, +) + +import platform + +is_osx = platform.mac_ver()[0] != '' + +import pyomo.common.unittest as unittest +import sys +import os + +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest.graphics as graphics + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" +) +@unittest.skipIf( + is_osx, + "Disabling graphics tests on OSX due to issue in Matplotlib, see Pyomo PR #1337", +) +class TestGraphics(unittest.TestCase): + def setUp(self): + self.A = pd.DataFrame( + np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') + ) + self.B = pd.DataFrame( + np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') + ) + + def test_pairwise_plot(self): + graphics.pairwise_plot(self.A, alpha=0.8, distributions=['Rect', 'MVN', 'KDE']) + + def test_grouped_boxplot(self): + graphics.grouped_boxplot(self.A, self.B, normalize=True, group_names=['A', 'B']) + + def test_grouped_violinplot(self): + graphics.grouped_violinplot(self.A, self.B) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py new file mode 100644 index 00000000000..7e692989b0c --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py @@ -0,0 +1,958 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, + matplotlib, + matplotlib_available, +) + +import platform + +is_osx = platform.mac_ver()[0] != "" + +import pyomo.common.unittest as unittest +import sys +import os +import subprocess +from itertools import product + +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest.graphics as graphics +import pyomo.contrib.parmest as parmestbase +import pyomo.environ as pyo +import pyomo.dae as dae + +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + +from pyomo.common.fileutils import find_library + +pynumero_ASL_available = False if find_library("pynumero_ASL") is None else True + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestRooneyBiegler(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, + ) + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est(self): + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_bootstrap(self): + objval, thetavals = self.pest.theta_est() + + num_bootstraps = 10 + theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) + + num_samples = theta_est["samples"].apply(len) + self.assertTrue(len(theta_est.index), 10) + self.assertTrue(num_samples.equals(pd.Series([6] * 10))) + + del theta_est["samples"] + + # apply confidence region test + CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) + + self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) + self.assertTrue(CR[0.5].sum() == 5) + self.assertTrue(CR[0.75].sum() == 7) + self.assertTrue(CR[1.0].sum() == 10) # all true + + graphics.pairwise_plot(theta_est) + graphics.pairwise_plot(theta_est, thetavals) + graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_likelihood_ratio(self): + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) + + self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) + self.assertTrue(LR[0.8].sum() == 6) + self.assertTrue(LR[0.9].sum() == 10) + self.assertTrue(LR[1.0].sum() == 60) # all true + + graphics.pairwise_plot(LR, thetavals, 0.8) + + def test_leaveNout(self): + lNo_theta = self.pest.theta_est_leaveNout(1) + self.assertTrue(lNo_theta.shape == (6, 2)) + + results = self.pest.leaveNout_bootstrap_test( + 1, None, 3, "Rect", [0.5, 1.0], seed=5436 + ) + self.assertTrue(len(results) == 6) # 6 lNo samples + i = 1 + samples = results[i][0] # list of N samples that are left out + lno_theta = results[i][1] + bootstrap_theta = results[i][2] + self.assertTrue(samples == [1]) # sample 1 was left out + self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 + self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) + self.assertTrue(lno_theta[1.0].sum() == 1) # all true + self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 + self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true + + def test_diagnostic_mode(self): + self.pest.diagnostic_mode = True + + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + self.pest.diagnostic_mode = False + + @unittest.skip("Presently having trouble with mpiexec on appveyor") + def test_parallel_parmest(self): + """use mpiexec and mpi4py""" + p = str(parmestbase.__path__) + l = p.find("'") + r = p.find("'", l + 1) + parmestpath = p[l + 1 : r] + rbpath = ( + parmestpath + + os.sep + + "examples" + + os.sep + + "rooney_biegler" + + os.sep + + "rooney_biegler_parmest.py" + ) + rbpath = os.path.abspath(rbpath) # paranoia strikes deep... + rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] + if sys.version_info >= (3, 5): + ret = subprocess.run(rlist) + retcode = ret.returncode + else: + retcode = subprocess.call(rlist) + assert retcode == 0 + + @unittest.skip("Most folks don't have k_aug installed") + def test_theta_k_aug_for_Hessian(self): + # this will fail if k_aug is not installed + objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") + self.assertAlmostEqual(objval, 4.4675, places=2) + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_theta_est_cov(self): + objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + # Covariance matrix + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper + + """ Why does the covariance matrix from parmest not match the paper? Parmest is + calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely + employed the first order approximation common for nonlinear regression. The paper + values were verified with Scipy, which uses the same first order approximation. + The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in + "Nonlinear Parameter Estimation", Y. Bard, 1974. + """ + + def test_cov_scipy_least_squares_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + def model(theta, t): + """ + Model to be fitted y = model(theta, t) + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + + Returns: + y: model predictions [need to check paper for units] + """ + asymptote = theta[0] + rate_constant = theta[1] + + return asymptote * (1 - np.exp(-rate_constant * t)) + + def residual(theta, t, y): + """ + Calculate residuals + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + y: dependent variable [?] + """ + return y - model(theta, t) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + ## solve with optimize.least_squares + sol = scipy.optimize.least_squares( + residual, theta_guess, method="trf", args=(t, y), verbose=2 + ) + theta_hat = sol.x + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + # calculate residuals + r = residual(theta_hat, t, y) + + # calculate variance of the residuals + # -2 because there are 2 fitted parameters + sigre = np.matmul(r.T, r / (len(y) - 2)) + + # approximate covariance + # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 + cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + def test_cov_scipy_curve_fit_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + ## solve with optimize.curve_fit + def model(t, asymptote, rate_constant): + return asymptote * (1 - np.exp(-rate_constant * t)) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestModelVariants(unittest.TestCase): + def setUp(self): + self.data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + def rooney_biegler_params(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Param(initialize=15, mutable=True) + model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_params(data): + model = pyo.ConcreteModel() + + model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Param( + model.param_names, + initialize={"asymptote": 15, "rate_constant": 0.5}, + mutable=True, + ) + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_vars(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.asymptote.fixed = True # parmest will unfix theta variables + model.rate_constant.fixed = True + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_vars(data): + model = pyo.ConcreteModel() + + model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Var( + model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} + ) + model.theta[ + "asymptote" + ].fixed = ( + True # parmest will unfix theta variables, even when they are indexed + ) + model.theta["rate_constant"].fixed = True + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + self.objective_function = SSE + + theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T + theta_vals_index = pd.DataFrame( + [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] + ).T + + self.input = { + "param": { + "model": rooney_biegler_params, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "param_index": { + "model": rooney_biegler_indexed_params, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars": { + "model": rooney_biegler_vars, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "vars_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars_quoted_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, + }, + "vars_str_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, + }, + } + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_parmest_basics(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_initialize_parmest_model_option(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, + ) + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + solver_options = {"max_iter": 6000} + + self.pest = parmest.Estimator( + reactor_design_model, data, theta_names, SSE, solver_options=solver_options + ) + + def test_theta_est(self): + # used in data reconciliation + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) + self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) + self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) + + def test_return_values(self): + objval, thetavals, data_rec = self.pest.theta_est( + return_values=["ca", "cb", "cc", "cd", "caf"] + ) + self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign_DAE(unittest.TestCase): + # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html + + def setUp(self): + def ABC_model(data): + ca_meas = data["ca"] + cb_meas = data["cb"] + cc_meas = data["cc"] + + if isinstance(data, pd.DataFrame): + meas_t = data.index # time index + else: # dictionary + meas_t = list(ca_meas.keys()) # nested dictionary + + ca0 = 1.0 + cb0 = 0.0 + cc0 = 0.0 + + m = pyo.ConcreteModel() + + m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) + m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) + + m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) + + # initialization and bounds + m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) + m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) + m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) + + m.dca = dae.DerivativeVar(m.ca, wrt=m.time) + m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) + m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) + + def _dcarate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dca[t] == -m.k1 * m.ca[t] + + m.dcarate = pyo.Constraint(m.time, rule=_dcarate) + + def _dcbrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] + + m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) + + def _dccrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcc[t] == m.k2 * m.cb[t] + + m.dccrate = pyo.Constraint(m.time, rule=_dccrate) + + def ComputeFirstStageCost_rule(m): + return 0 + + m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) + + def ComputeSecondStageCost_rule(m): + return sum( + (m.ca[t] - ca_meas[t]) ** 2 + + (m.cb[t] - cb_meas[t]) ** 2 + + (m.cc[t] - cc_meas[t]) ** 2 + for t in meas_t + ) + + m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = pyo.Objective( + rule=total_cost_rule, sense=pyo.minimize + ) + + disc = pyo.TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=2) + + return m + + # This example tests data formatted in 3 ways + # Each format holds 1 scenario + # 1. dataframe with time index + # 2. nested dictionary {ca: {t, val pairs}, ... } + data = [ + [0.000, 0.957, -0.031, -0.015], + [0.263, 0.557, 0.330, 0.044], + [0.526, 0.342, 0.512, 0.156], + [0.789, 0.224, 0.499, 0.310], + [1.053, 0.123, 0.428, 0.454], + [1.316, 0.079, 0.396, 0.556], + [1.579, 0.035, 0.303, 0.651], + [1.842, 0.029, 0.287, 0.658], + [2.105, 0.025, 0.221, 0.750], + [2.368, 0.017, 0.148, 0.854], + [2.632, -0.002, 0.182, 0.845], + [2.895, 0.009, 0.116, 0.893], + [3.158, -0.023, 0.079, 0.942], + [3.421, 0.006, 0.078, 0.899], + [3.684, 0.016, 0.059, 0.942], + [3.947, 0.014, 0.036, 0.991], + [4.211, -0.009, 0.014, 0.988], + [4.474, -0.030, 0.036, 0.941], + [4.737, 0.004, 0.036, 0.971], + [5.000, -0.024, 0.028, 0.985], + ] + data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) + data_df = data.set_index("t") + data_dict = { + "ca": {k: v for (k, v) in zip(data.t, data.ca)}, + "cb": {k: v for (k, v) in zip(data.t, data.cb)}, + "cc": {k: v for (k, v) in zip(data.t, data.cc)}, + } + + theta_names = ["k1", "k2"] + + self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) + self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) + + # Estimator object with multiple scenarios + self.pest_df_multiple = parmest.Estimator( + ABC_model, [data_df, data_df], theta_names + ) + self.pest_dict_multiple = parmest.Estimator( + ABC_model, [data_dict, data_dict], theta_names + ) + + # Create an instance of the model + self.m_df = ABC_model(data_df) + self.m_dict = ABC_model(data_dict) + + def test_dataformats(self): + obj1, theta1 = self.pest_df.theta_est() + obj2, theta2 = self.pest_dict.theta_est() + + self.assertAlmostEqual(obj1, obj2, places=6) + self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) + self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) + + def test_return_continuous_set(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) + obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) + self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) + + def test_return_continuous_set_multiple_datasets(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( + return_values=["time"] + ) + obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( + return_values=["time"] + ) + self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + + def test_covariance(self): + from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + inv_reduced_hessian_barrier, + ) + + # Number of datapoints. + # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # In this example, this is the number of data points in data_df, but that's + # only because the data is indexed by time and contains no additional information. + n = 60 + + # Compute covariance using parmest + obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # Compute covariance using interior_point + vars_list = [self.m_df.k1, self.m_df.k2] + solve_result, inv_red_hes = inv_reduced_hessian_barrier( + self.m_df, independent_variables=vars_list, tee=True + ) + l = len(vars_list) + cov_interior_point = 2 * obj / (n - l) * inv_red_hes + cov_interior_point = pd.DataFrame( + cov_interior_point, ["k1", "k2"], ["k1", "k2"] + ) + + cov_diff = (cov - cov_interior_point).abs().sum().sum() + + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) + self.assertAlmostEqual(cov_diff, 0, places=6) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSquareInitialization_RooneyBiegler(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( + rooney_biegler_model_with_constraint, + ) + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model_with_constraint, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est_with_square_initialization(self): + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_and_custom_init_theta(self): + theta_vals_init = pd.DataFrame( + data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] + ) + obj_init = self.pest.objective_at_theta( + theta_values=theta_vals_init, initialize_parmest_model=True + ) + objval, thetavals = self.pest.theta_est() + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_diagnostic_mode_true(self): + self.pest.diagnostic_mode = True + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + self.pest.diagnostic_mode = False + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py new file mode 100644 index 00000000000..22a851ae32e --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py @@ -0,0 +1,146 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import pandas as pd, pandas_available + +uuid_available = True +try: + import uuid +except: + uuid_available = False + +import pyomo.common.unittest as unittest +import os +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest.scenariocreator as sc +import pyomo.environ as pyo +from pyomo.environ import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioReactorDesign(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, + ) + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + def test_scen_from_exps(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv("delme_exp_csv.csv") + df = pd.read_csv("delme_exp_csv.csv") + os.remove("delme_exp_csv.csv") + # March '20: all reactor_design experiments have the same theta values! + k1val = df.loc[5].at["k1"] + self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) + tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) + + @unittest.skipIf(not uuid_available, "The uuid module is not available") + def test_no_csv_if_empty(self): + # low level test of scenario sets + # verify that nothing is written, but no errors with empty set + + emptyset = sc.ScenarioSet("empty") + tfile = uuid.uuid4().hex + ".csv" + emptyset.write_csv(tfile) + self.assertFalse( + os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" + ) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioSemibatch(unittest.TestCase): + def setUp(self): + import pyomo.contrib.parmest.examples.semibatch.semibatch as sb + import json + + # Vars to estimate in parmest + theta_names = ["k1", "k2", "E1", "E2"] + + self.fbase = os.path.join(testdir, "..", "examples", "semibatch") + # Data, list of dictionaries + data = [] + for exp_num in range(10): + fname = "exp" + str(exp_num + 1) + ".out" + fullname = os.path.join(self.fbase, fname) + with open(fullname, "r") as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for the sum of squared error that will be used in parameter estimation + + self.pest = parmest.Estimator(sb.generate_model, data, theta_names) + + def test_semibatch_bootstrap(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + bootscens = sc.ScenarioSet("Bootstrap") + numtomake = 2 + scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) + tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 20.64, places=1) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_solver.py b/pyomo/contrib/parmest/deprecated/tests/test_solver.py new file mode 100644 index 00000000000..eb655023b9b --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_solver.py @@ -0,0 +1,75 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, + matplotlib, + matplotlib_available, +) + +import platform + +is_osx = platform.mac_ver()[0] != '' + +import pyomo.common.unittest as unittest +import os + +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest as parmestbase +import pyomo.environ as pyo + +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory('ipopt').available() + +from pyomo.common.fileutils import find_library + +pynumero_ASL_available = False if find_library('pynumero_ASL') is None else True + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSolver(unittest.TestCase): + def setUp(self): + pass + + def test_ipopt_solve_with_stats(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, + ) + from pyomo.contrib.parmest.utils import ipopt_solve_with_stats + + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + model = rooney_biegler_model(data) + solver = pyo.SolverFactory('ipopt') + solver.solve(model) + + status_obj, solved, iters, time, regu = ipopt_solve_with_stats(model, solver) + + self.assertEqual(solved, True) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_utils.py b/pyomo/contrib/parmest/deprecated/tests/test_utils.py new file mode 100644 index 00000000000..514c14b1e82 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_utils.py @@ -0,0 +1,68 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import pandas as pd, pandas_available + +import pyomo.environ as pyo +import pyomo.common.unittest as unittest +import pyomo.contrib.parmest.parmest as parmest +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestUtils(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + @unittest.pytest.mark.expensive + def test_convert_param_to_var(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, + ) + + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + instance = reactor_design_model(data.loc[0]) + solver = pyo.SolverFactory("ipopt") + solver.solve(instance) + + instance_vars = parmest.utils.convert_params_to_vars( + instance, theta_names, fix_vars=True + ) + solver.solve(instance_vars) + + assert instance.k1() == instance_vars.k1() + assert instance.k2() == instance_vars.k2() + assert instance.k3() == instance_vars.k3() + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index dc747217b31..1f9b8b645b8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -63,7 +63,7 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet -import pyomo.contrib.parmest.parmest_deprecated as parmest_deprecated +import pyomo.contrib.parmest.deprecated.parmest as parmest_deprecated parmest_available = numpy_available & pandas_available & scipy_available @@ -398,14 +398,21 @@ def _return_theta_names(self): """ Return list of fitted model parameter names """ - # if fitted model parameter names differ from theta_names created when Estimator object is created - if hasattr(self, 'theta_names_updated'): - return self.theta_names_updated + # check for deprecated inputs + if self.pest_deprecated is not None: + + # if fitted model parameter names differ from theta_names + # created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.pest_deprecated.theta_names_updated + + else: + return ( + self.pest_deprecated.theta_names + ) # default theta_names, created when Estimator object is created else: - return ( - self.theta_names - ) # default theta_names, created when Estimator object is created + return None def _create_parmest_model(self, data): """ diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 18c27ad1c86..b849bfdfd5b 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,7 +14,7 @@ import pyomo.environ as pyo -import pyomo.contrib.parmest.scenariocreator_deprecated as scen_deprecated +import pyomo.contrib.parmest.deprecated.scenariocreator as scen_deprecated import logging logger = logging.getLogger(__name__) From 475ec06fb243b9afa4f59a8cefbbacd56d6634f3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 10:35:33 -0700 Subject: [PATCH 0179/1178] simplification tests --- .../simplification/ginac_interface.cpp | 2 +- pyomo/contrib/simplification/simplify.py | 5 +- .../tests/test_simplification.py | 79 ++++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 690885dc513..32bea8dadd0 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -21,7 +21,7 @@ ex ginac_expr_from_pyomo_node( case py_float: { double val = expr.cast(); if (is_integer(val)) { - res = numeric(expr.cast()); + res = numeric((long) val); } else { res = numeric(val); diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 938bff6b4b9..66a3dad0b06 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -25,7 +25,10 @@ def simplify_with_sympy(expr: NumericExpression): def simplify_with_ginac(expr: NumericExpression, ginac_interface): gi = ginac_interface - return gi.from_ginac(gi.to_ginac(expr).normal()) + ginac_expr = gi.to_ginac(expr) + ginac_expr = ginac_expr.normal() + new_expr = gi.from_ginac(ginac_expr) + return new_expr class Simplifier(object): diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 02107ba1d6c..4d9b0cec0d2 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -6,6 +6,14 @@ class TestSimplification(TestCase): + def compare_against_possible_results(self, got, expected_list): + success = False + for exp in expected_list: + if compare_expressions(got, exp): + success = True + break + self.assertTrue(success) + def test_simplify(self): m = pe.ConcreteModel() x = m.x = pe.Var(bounds=(0, None)) @@ -24,13 +32,16 @@ def test_param(self): e1 = p*x**2 + p*x + p*x**2 simp = Simplifier() e2 = simp.simplify(e1) - exp1 = p*x**2.0*2.0 + p*x - exp2 = p*x + p*x**2.0*2.0 - self.assertTrue( - compare_expressions(e2, exp1) - or compare_expressions(e2, exp2) - or compare_expressions(e2, p*x + x**2.0*p*2.0) - or compare_expressions(e2, x**2.0*p*2.0 + p*x) + self.compare_against_possible_results( + e2, + [ + p*x**2.0*2.0 + p*x, + p*x + p*x**2.0*2.0, + 2.0*p*x**2.0 + p*x, + p*x + 2.0*p*x**2.0, + x**2.0*p*2.0 + p*x, + p*x + x**2.0*p*2.0 + ] ) def test_mul(self): @@ -48,8 +59,13 @@ def test_sum(self): e = 2 + x simp = Simplifier() e2 = simp.simplify(e) - expected = x + 2.0 - assertExpressionsEqual(self, expected, e2) + self.compare_against_possible_results( + e2, + [ + 2.0 + x, + x + 2.0, + ] + ) def test_neg(self): m = pe.ConcreteModel() @@ -57,6 +73,47 @@ def test_neg(self): e = -pe.log(x) simp = Simplifier() e2 = simp.simplify(e) - expected = pe.log(x)*(-1.0) - assertExpressionsEqual(self, expected, e2) + self.compare_against_possible_results( + e2, + [ + (-1.0)*pe.log(x), + pe.log(x)*(-1.0), + -pe.log(x), + ] + ) + + def test_pow(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = x**2.0 + simp = Simplifier() + e2 = simp.simplify(e) + assertExpressionsEqual(self, e, e2) + def test_div(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + y = m.y = pe.Var() + e = x/y + y/x - x/y + simp = Simplifier() + e2 = simp.simplify(e) + print(e2) + self.compare_against_possible_results( + e2, + [ + y/x, + y*(1.0/x), + y*x**-1.0, + x**-1.0 * y, + ], + ) + + def test_unary(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + func_list = [pe.log, pe.sin, pe.cos, pe.tan, pe.asin, pe.acos, pe.atan] + for func in func_list: + e = func(x) + simp = Simplifier() + e2 = simp.simplify(e) + assertExpressionsEqual(self, e, e2) From 491db9f6793dc6d84fc3b771073d00d938b3a2f2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 15:57:34 -0700 Subject: [PATCH 0180/1178] update GHA to install ginac --- .github/workflows/test_pr_and_main.yml | 21 ++++++++++++++++++- .../tests/test_simplification.py | 2 ++ setup.cfg | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2885fd107a8..12dc7c1daac 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -179,6 +179,25 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} + - name: install ginac + if: ${{ matrix.other == "singletest" }} + run: | + pwd + cd .. + curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 + tar -xvf cln-1.3.6.tar.bz2 + cd cln-1.3.6 + ./configure + make + make install + cd + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure + make + make install + - name: TPL package download cache uses: actions/cache@v3 if: ${{ ! matrix.slim }} diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 4d9b0cec0d2..ed59064022c 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,10 +1,12 @@ from pyomo.common.unittest import TestCase +from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index b606138f38c..a431e0cd601 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + simplification: marks simplification tests that have expensive (to install) dependencies From b3a1ff9b06e3fb2e8fd944bb27d2bf736a9cfd54 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:02:28 -0700 Subject: [PATCH 0181/1178] run black --- pyomo/contrib/simplification/build.py | 16 +++--- pyomo/contrib/simplification/simplify.py | 2 + .../tests/test_simplification.py | 49 ++++++------------- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 6f16607e22b..e8bd645756b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -11,16 +11,16 @@ def build_ginac_interface(args=[]): dname = this_file_dir() - _sources = [ - 'ginac_interface.cpp', - ] + _sources = ['ginac_interface.cpp'] sources = list() for fname in _sources: sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') if ginac_lib is None: - raise RuntimeError('could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable') + raise RuntimeError( + 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' + ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) ginac_include_dir = os.path.join(ginac_build_dir, 'include') @@ -29,7 +29,9 @@ def build_ginac_interface(args=[]): cln_lib = find_library('cln') if cln_lib is None: - raise RuntimeError('could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable') + raise RuntimeError( + 'could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable' + ) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) cln_include_dir = os.path.join(cln_build_dir, 'include') @@ -38,8 +40,8 @@ def build_ginac_interface(args=[]): extra_args = ['-std=c++11'] ext = Pybind11Extension( - 'ginac_interface', - sources=sources, + 'ginac_interface', + sources=sources, language='c++', include_dirs=[cln_include_dir, ginac_include_dir], library_dirs=[cln_lib_dir, ginac_lib_dir], diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 66a3dad0b06..8f7f15f3826 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -3,8 +3,10 @@ from pyomo.core.expr.numvalue import is_fixed, value import logging import warnings + try: from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True except: GinacInterface = None diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index ed59064022c..cc278db4d43 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -19,7 +19,7 @@ def compare_against_possible_results(self, got, expected_list): def test_simplify(self): m = pe.ConcreteModel() x = m.x = pe.Var(bounds=(0, None)) - e = x*pe.log(x) + e = x * pe.log(x) der1 = reverse_sd(e)[x] der2 = reverse_sd(der1)[x] simp = Simplifier() @@ -31,28 +31,28 @@ def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() p = m.p = pe.Param(mutable=True) - e1 = p*x**2 + p*x + p*x**2 + e1 = p * x**2 + p * x + p * x**2 simp = Simplifier() e2 = simp.simplify(e1) self.compare_against_possible_results( - e2, + e2, [ - p*x**2.0*2.0 + p*x, - p*x + p*x**2.0*2.0, - 2.0*p*x**2.0 + p*x, - p*x + 2.0*p*x**2.0, - x**2.0*p*2.0 + p*x, - p*x + x**2.0*p*2.0 - ] + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + ], ) def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() - e = 2*x + e = 2 * x simp = Simplifier() e2 = simp.simplify(e) - expected = 2.0*x + expected = 2.0 * x assertExpressionsEqual(self, expected, e2) def test_sum(self): @@ -61,13 +61,7 @@ def test_sum(self): e = 2 + x simp = Simplifier() e2 = simp.simplify(e) - self.compare_against_possible_results( - e2, - [ - 2.0 + x, - x + 2.0, - ] - ) + self.compare_against_possible_results(e2, [2.0 + x, x + 2.0]) def test_neg(self): m = pe.ConcreteModel() @@ -76,12 +70,7 @@ def test_neg(self): simp = Simplifier() e2 = simp.simplify(e) self.compare_against_possible_results( - e2, - [ - (-1.0)*pe.log(x), - pe.log(x)*(-1.0), - -pe.log(x), - ] + e2, [(-1.0) * pe.log(x), pe.log(x) * (-1.0), -pe.log(x)] ) def test_pow(self): @@ -96,18 +85,12 @@ def test_div(self): m = pe.ConcreteModel() x = m.x = pe.Var() y = m.y = pe.Var() - e = x/y + y/x - x/y + e = x / y + y / x - x / y simp = Simplifier() e2 = simp.simplify(e) print(e2) self.compare_against_possible_results( - e2, - [ - y/x, - y*(1.0/x), - y*x**-1.0, - x**-1.0 * y, - ], + e2, [y / x, y * (1.0 / x), y * x**-1.0, x**-1.0 * y] ) def test_unary(self): From de8743a84c4902867a802756ab64fbd62eed920e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:03:42 -0700 Subject: [PATCH 0182/1178] syntax --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 12dc7c1daac..7345fd45e10 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -180,7 +180,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == "singletest" }} + if: ${{ matrix.other == 'singletest' }} run: | pwd cd .. From ee9f830984ad20446b28c8aa65674d5cbf264ab3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:07:25 -0700 Subject: [PATCH 0183/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e773587ec85..3270b3e8a95 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -94,6 +94,14 @@ jobs: PYENV: conda PACKAGES: mpi4py + - os: ubuntu-latest + python: 3.11 + other: /singletest + category: "-m 'neos or importtest or simplification'" + skip_doctest: 1 + TARGET: linux + PYENV: pip + - os: ubuntu-latest python: '3.10' other: /cython @@ -149,6 +157,25 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} + - name: install ginac + if: ${{ matrix.other == 'singletest' }} + run: | + pwd + cd .. + curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 + tar -xvf cln-1.3.6.tar.bz2 + cd cln-1.3.6 + ./configure + make + make install + cd + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure + make + make install + - name: TPL package download cache uses: actions/cache@v3 if: ${{ ! matrix.slim }} From 36cfd6388d16d6f2077d51b9035c9e363b4e4e28 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:09:53 -0700 Subject: [PATCH 0184/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3270b3e8a95..97b9c6ed1dc 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == 'singletest' }} + if: matrix.TARGET == 'singletest' run: | pwd cd .. From 8269539ff67b48e4c7b652b8feabb14c64634fc6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:11:27 -0700 Subject: [PATCH 0185/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 97b9c6ed1dc..c56ec398134 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: matrix.TARGET == 'singletest' + if: matrix.other == 'singletest' run: | pwd cd .. From 7bb0ff501344dfc9bb162f9ed339293ad320d0d1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:14:07 -0700 Subject: [PATCH 0186/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- pyomo/contrib/simplification/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index c56ec398134..bea57f314e5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: matrix.other == 'singletest' + if: matrix.other == '/singletest' run: | pwd cd .. diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index c09e8b8b5e5..3abe5a25ba0 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1 +1 @@ -from .simplify import Simplifier \ No newline at end of file +from .simplify import Simplifier From 546dad1d46cc1a60c6fae711a6dbe8710f53220c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:23:06 -0700 Subject: [PATCH 0187/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 10 ++++++++-- pyomo/contrib/simplification/simplify.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index bea57f314e5..16ce6a44003 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -167,14 +167,14 @@ jobs: cd cln-1.3.6 ./configure make - make install + sudo make install cd curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure make - make install + sudo make install - name: TPL package download cache uses: actions/cache@v3 @@ -630,6 +630,12 @@ jobs: echo "" pyomo build-extensions --parallel 2 + - name: Install GiNaC Interface + if: matrix.other == '/singletest' + run: | + cd pyomo/contrib/simplification/ + $PYTHON_EXE build.py --inplace + - name: Report pyomo plugin information run: | echo "$PATH" diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 8f7f15f3826..4002f1a233f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -34,10 +34,10 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): - def __init__(self, supress_no_ginac_warnings: bool = False) -> None: + def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: if ginac_available: self.gi = GinacInterface(False) - self.suppress_no_ginac_warnings = supress_no_ginac_warnings + self.suppress_no_ginac_warnings = suppress_no_ginac_warnings def simplify(self, expr: NumericExpression): if ginac_available: From 37a955bdfaccec8508887dbf037eb5ea72b5b5cb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:24:16 -0700 Subject: [PATCH 0188/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 16ce6a44003..477361683ac 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -634,7 +634,7 @@ jobs: if: matrix.other == '/singletest' run: | cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace + $PYTHON_EXE build.py --inplace - name: Report pyomo plugin information run: | From 05134ce49621f1efd16d961dc20e34c7cff23475 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:45:55 -0700 Subject: [PATCH 0189/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 1 + pyomo/contrib/simplification/build.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 477361683ac..7933aa522d8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -633,6 +633,7 @@ jobs: - name: Install GiNaC Interface if: matrix.other == '/singletest' run: | + ls /usr/local/include/ginac/ cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index e8bd645756b..39742e1e351 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -17,6 +17,7 @@ def build_ginac_interface(args=[]): sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') + print(ginac_lib) if ginac_lib is None: raise RuntimeError( 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' From 1151927df204f6969704ec7b671e25a1d9083031 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 17:51:34 -0700 Subject: [PATCH 0190/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 7933aa522d8..f2bf057da51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -633,7 +633,7 @@ jobs: - name: Install GiNaC Interface if: matrix.other == '/singletest' run: | - ls /usr/local/include/ginac/ + export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace From 2c4fdbee83ab76b35a863748048ddf0d091f2f3c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:19:05 -0700 Subject: [PATCH 0191/1178] skip tests when dependencies are not available --- pyomo/contrib/simplification/tests/test_simplification.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index cc278db4d43..c50a906afe7 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,11 +1,17 @@ from pyomo.common.unittest import TestCase from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier +from pyomo.contrib.simplification.simplify import ginac_available from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.common.dependencies import attempt_import +sympy, sympy_available = attempt_import('sympy') + + +@unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') @unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): From 9ebd79b898aea6827232220f59c615c771686ddb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:26:40 -0700 Subject: [PATCH 0192/1178] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index f2bf057da51..b97b3a682af 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -168,7 +168,7 @@ jobs: ./configure make sudo make install - cd + cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 7345fd45e10..b99d39cf6c7 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -180,23 +180,22 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == 'singletest' }} + if: matrix.other == '/singletest' run: | - pwd cd .. curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure make - make install - cd + sudo make install + cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure make - make install + sudo make install - name: TPL package download cache uses: actions/cache@v3 @@ -652,6 +651,13 @@ jobs: echo "" pyomo build-extensions --parallel 2 + - name: Install GiNaC Interface + if: matrix.other == '/singletest' + run: | + export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + cd pyomo/contrib/simplification/ + $PYTHON_EXE build.py --inplace + - name: Report pyomo plugin information run: | echo "$PATH" From 0bd156351b09d93288d618715817fcc4c530114a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:28:39 -0700 Subject: [PATCH 0193/1178] update simplification tests --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 1 - setup.cfg | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b97b3a682af..13a5653f9f5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index b99d39cf6c7..8a8a9b08030 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index c50a906afe7..f3bce9cee54 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -12,7 +12,6 @@ @unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') -@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index a431e0cd601..b606138f38c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,3 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - simplification: marks simplification tests that have expensive (to install) dependencies From 26007ac42688cbe7554807198ceb20e9d121d68e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:30:57 -0700 Subject: [PATCH 0194/1178] run black --- pyomo/contrib/simplification/tests/test_simplification.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index f3bce9cee54..e6b5ae863f6 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -11,7 +11,10 @@ sympy, sympy_available = attempt_import('sympy') -@unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') +@unittest.skipIf( + (not sympy_available) and (not ginac_available), + 'neither sympy nor ginac are available', +) class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False From fc36411c1c57aaf4720938cf7926cfcf7048bc75 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:56:21 -0700 Subject: [PATCH 0195/1178] add pytest marker for simplification --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 1 + setup.cfg | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 13a5653f9f5..b97b3a682af 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 8a8a9b08030..b99d39cf6c7 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index e6b5ae863f6..152db93a358 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -15,6 +15,7 @@ (not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available', ) +@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index b606138f38c..855717490b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + simplification: tests for expression simplification that have expensive (to install) dependencies From 5ba03e215c51fe2feba6a5cff7b2baa162fcb87f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:30:50 -0700 Subject: [PATCH 0196/1178] update GHA --- .github/workflows/test_branches.yml | 1 + .github/workflows/test_pr_and_main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 243f57ea7aa..b73a9cabc81 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -641,6 +641,7 @@ jobs: if: matrix.other == '/singletest' run: | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 93919ca6bc3..1c36b89710c 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -662,6 +662,7 @@ jobs: if: matrix.other == '/singletest' run: | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace From 90cdeba86b2deaa76ce7b62cd9f21f906b057462 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:32:33 -0700 Subject: [PATCH 0197/1178] update GHA --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b73a9cabc81..e3e0c3e6caf 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -166,14 +166,14 @@ jobs: tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure - make + make -j 2 sudo make install cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure - make + make -j 2 sudo make install - name: TPL package download cache diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 1c36b89710c..9edb8b1c65f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -187,14 +187,14 @@ jobs: tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure - make + make -j 2 sudo make install cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure - make + make -j 2 sudo make install - name: TPL package download cache From 17cb11d31d72d7ac9ac9f37e1ec306f8d114cec4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:50:50 -0700 Subject: [PATCH 0198/1178] debugging GHA --- .github/workflows/test_branches.yml | 1 + .github/workflows/test_pr_and_main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e3e0c3e6caf..4e3c14cb70e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,6 +655,7 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9edb8b1c65f..1626964a7e9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -676,6 +676,7 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From d1fe24400ed424afdfdf65e3f9918faefc98db96 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 20:02:34 -0700 Subject: [PATCH 0199/1178] test simplification with ginac and sympy --- .../simplification/tests/test_simplification.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 152db93a358..096d776460d 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -12,11 +12,10 @@ @unittest.skipIf( - (not sympy_available) and (not ginac_available), - 'neither sympy nor ginac are available', + (not sympy_available) or (ginac_available), + 'sympy is not available', ) -@unittest.pytest.mark.simplification -class TestSimplification(TestCase): +class TestSimplificationSympy(TestCase): def compare_against_possible_results(self, got, expected_list): success = False for exp in expected_list: @@ -111,3 +110,12 @@ def test_unary(self): simp = Simplifier() e2 = simp.simplify(e) assertExpressionsEqual(self, e, e2) + + +@unittest.skipIf( + not ginac_available, + 'GiNaC is not available', +) +@unittest.pytest.mark.simplification +class TestSimplificationGiNaC(TestSimplificationSympy): + pass From 9159c3c5854c7a9d5437f3308321a9fd4a61be6f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 20:03:18 -0700 Subject: [PATCH 0200/1178] test simplification with ginac and sympy --- .../tests/test_simplification.py | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 096d776460d..3124d856784 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -35,25 +35,6 @@ def test_simplify(self): expected = x**-1.0 assertExpressionsEqual(self, expected, der2_simp) - def test_param(self): - m = pe.ConcreteModel() - x = m.x = pe.Var() - p = m.p = pe.Param(mutable=True) - e1 = p * x**2 + p * x + p * x**2 - simp = Simplifier() - e2 = simp.simplify(e1) - self.compare_against_possible_results( - e2, - [ - p * x**2.0 * 2.0 + p * x, - p * x + p * x**2.0 * 2.0, - 2.0 * p * x**2.0 + p * x, - p * x + 2.0 * p * x**2.0, - x**2.0 * p * 2.0 + p * x, - p * x + x**2.0 * p * 2.0, - ], - ) - def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() @@ -118,4 +99,21 @@ def test_unary(self): ) @unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestSimplificationSympy): - pass + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p * x**2 + p * x + p * x**2 + simp = Simplifier() + e2 = simp.simplify(e1) + self.compare_against_possible_results( + e2, + [ + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + ], + ) From 457f2b378b772eae16b47fa789423bca70de43c5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:17:46 -0700 Subject: [PATCH 0201/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 4e3c14cb70e..0eb6fe166d9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,7 +655,8 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" + $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From 1e7c3f1b2ecf562000088df80f215ddf6d992420 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:44:54 -0700 Subject: [PATCH 0202/1178] fixing simplification tests --- .../simplification/tests/test_simplification.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 3124d856784..6badc76b957 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -11,11 +11,7 @@ sympy, sympy_available = attempt_import('sympy') -@unittest.skipIf( - (not sympy_available) or (ginac_available), - 'sympy is not available', -) -class TestSimplificationSympy(TestCase): +class SimplificationMixin: def compare_against_possible_results(self, got, expected_list): success = False for exp in expected_list: @@ -93,12 +89,20 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) +@unittest.skipIf( + (not sympy_available) or (ginac_available), + 'sympy is not available', +) +class TestSimplificationSympy(TestCase, SimplificationMixin): + pass + + @unittest.skipIf( not ginac_available, 'GiNaC is not available', ) @unittest.pytest.mark.simplification -class TestSimplificationGiNaC(TestSimplificationSympy): +class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() From b2d969f73e4dbf1e80d453dd7be4dcd474fc32be Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:46:36 -0700 Subject: [PATCH 0203/1178] fixing simplification tests --- .../simplification/tests/test_simplification.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 6badc76b957..e3c60cb02ca 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -89,18 +89,12 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) -@unittest.skipIf( - (not sympy_available) or (ginac_available), - 'sympy is not available', -) +@unittest.skipIf((not sympy_available) or (ginac_available), 'sympy is not available') class TestSimplificationSympy(TestCase, SimplificationMixin): pass -@unittest.skipIf( - not ginac_available, - 'GiNaC is not available', -) +@unittest.skipIf(not ginac_available, 'GiNaC is not available') @unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): From da2fe3d714b34c7b03a41a2c63af8590db87d2e4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 23:04:15 -0700 Subject: [PATCH 0204/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 0eb6fe166d9..1124a253ac8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -657,6 +657,7 @@ jobs: run: | $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" + pytest -v pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From 7828311676ecad4ae60600104333eac0a9c01a2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 11:49:14 -0700 Subject: [PATCH 0205/1178] Typo correction --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 202b0422cee..d7f4adabf56 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -405,7 +405,7 @@ def solve( self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit self.config.report_timing = report_timing - # This is a new flag in the interface. To preserve backwards compability, + # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" self.config.raise_exception_on_nonoptimal_result = ( raise_exception_on_nonoptimal_result From 5c452be23adc9cb9864c56176b60da32e068d2bc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 15 Jan 2024 12:07:54 -0700 Subject: [PATCH 0206/1178] minor updates --- pyomo/solver/config.py | 1 + pyomo/solver/ipopt.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 54a497cee0c..ef0114ba439 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -85,6 +85,7 @@ def __init__( ConfigValue( domain=NonNegativeInt, description="Number of threads to be used by a solver.", + default=None, ), ) self.time_limit: Optional[float] = self.declare( diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 406f4291c44..51f48ec4881 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -22,6 +22,7 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.core.base import Objective from pyomo.core.base.label import NumericLabeler +from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn from pyomo.solver.base import SolverBase, SymbolMap from pyomo.solver.config import SolverConfig @@ -83,9 +84,6 @@ def __init__( self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) - self.presolve: bool = self.declare( - 'presolve', ConfigValue(domain=bool, default=True) - ) class ipoptResults(Results): @@ -208,6 +206,10 @@ def version(self): version = tuple(int(i) for i in version.split('.')) return version + @property + def writer(self): + return self._writer + @property def config(self): return self._config @@ -269,14 +271,14 @@ def solve(self, model, **kwds): raise ipoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) + StaleFlagManager.mark_all_as_stale() # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) - self._writer.config.linear_presolve = config.presolve if config.threads: logger.log( logging.WARNING, - msg=f"The `threads` option was specified, but this has not yet been implemented for {self.__class__}.", + msg=f"The `threads` option was specified, but but is not used by {self.__class__}.", ) results = ipoptResults() with TempfileManager.new_context() as tempfile: From 8e168cfc3d6a55460f3a92f779d0340f649c7c6e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 15:21:44 -0700 Subject: [PATCH 0207/1178] MOVE: Shift pyomo.solver to pyomo.contrib.solver --- pyomo/contrib/appsi/fbbt.py | 2 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 8 ++++---- pyomo/contrib/appsi/solvers/cplex.py | 8 ++++---- pyomo/contrib/appsi/solvers/gurobi.py | 10 +++++----- pyomo/contrib/appsi/solvers/highs.py | 10 +++++----- pyomo/contrib/appsi/solvers/ipopt.py | 8 ++++---- .../appsi/solvers/tests/test_gurobi_persistent.py | 2 +- .../appsi/solvers/tests/test_persistent_solvers.py | 4 ++-- .../appsi/solvers/tests/test_wntr_persistent.py | 2 +- pyomo/contrib/appsi/solvers/wntr.py | 10 +++++----- pyomo/contrib/appsi/writers/lp_writer.py | 2 +- pyomo/contrib/appsi/writers/nl_writer.py | 2 +- pyomo/{ => contrib}/solver/__init__.py | 0 pyomo/{ => contrib}/solver/base.py | 6 +++--- pyomo/{ => contrib}/solver/config.py | 0 pyomo/{ => contrib}/solver/factory.py | 2 +- pyomo/{ => contrib}/solver/ipopt.py | 12 ++++++------ pyomo/{ => contrib}/solver/plugins.py | 0 pyomo/{ => contrib}/solver/results.py | 2 +- pyomo/{ => contrib}/solver/solution.py | 0 pyomo/{ => contrib}/solver/tests/__init__.py | 0 .../{ => contrib}/solver/tests/solvers/test_ipopt.py | 4 ++-- pyomo/{ => contrib}/solver/tests/unit/test_base.py | 0 pyomo/{ => contrib}/solver/tests/unit/test_config.py | 2 +- .../{ => contrib}/solver/tests/unit/test_results.py | 0 .../{ => contrib}/solver/tests/unit/test_solution.py | 0 pyomo/{ => contrib}/solver/tests/unit/test_util.py | 2 +- pyomo/{ => contrib}/solver/util.py | 4 ++-- pyomo/environ/__init__.py | 1 - 30 files changed, 52 insertions(+), 53 deletions(-) rename pyomo/{ => contrib}/solver/__init__.py (100%) rename pyomo/{ => contrib}/solver/base.py (99%) rename pyomo/{ => contrib}/solver/config.py (100%) rename pyomo/{ => contrib}/solver/factory.py (94%) rename pyomo/{ => contrib}/solver/ipopt.py (97%) rename pyomo/{ => contrib}/solver/plugins.py (100%) rename pyomo/{ => contrib}/solver/results.py (99%) rename pyomo/{ => contrib}/solver/solution.py (100%) rename pyomo/{ => contrib}/solver/tests/__init__.py (100%) rename pyomo/{ => contrib}/solver/tests/solvers/test_ipopt.py (94%) rename pyomo/{ => contrib}/solver/tests/unit/test_base.py (100%) rename pyomo/{ => contrib}/solver/tests/unit/test_config.py (96%) rename pyomo/{ => contrib}/solver/tests/unit/test_results.py (100%) rename pyomo/{ => contrib}/solver/tests/unit/test_solution.py (100%) rename pyomo/{ => contrib}/solver/tests/unit/test_util.py (97%) rename pyomo/{ => contrib}/solver/util.py (99%) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index cff1085de0d..ccbb3819554 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.util import PersistentSolverUtils from pyomo.common.config import ( ConfigDict, ConfigValue, diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 3a132b74395..ebccba09ab2 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.solver.factory import SolverFactory +from pyomo.contrib.solver.factory import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 62404890d0b..141c6de57bd 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -22,10 +22,10 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import SolverConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1837b5690a0..6f02ac12eb1 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -19,10 +19,10 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import BranchAndBoundConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 99fa19820a5..a947c8d7d7d 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -22,11 +22,11 @@ from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import BranchAndBoundConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index b270e4f2700..1680831471c 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,11 +20,11 @@ from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import BranchAndBoundConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 569bb98457f..ec59b827192 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -26,10 +26,10 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import SolverConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index c1825879dbe..4619a1c5452 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,7 +1,7 @@ from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.solver.results import TerminationCondition +from pyomo.contrib.solver.results import TerminationCondition from pyomo.core.expr.taylor_series import taylor_series_expansion diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index b50a072abbd..6731eb645fa 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,8 +4,8 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.results import TerminationCondition, Results from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index 971305001a9..e09865294eb 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.solver.results import TerminationCondition, SolutionStatus +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index aaa130f8631..04f54530c1b 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,8 +1,8 @@ -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.util import PersistentSolverUtils -from pyomo.solver.config import SolverConfig -from pyomo.solver.results import Results, TerminationCondition, SolutionStatus -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.contrib.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( ProductExpression, DivisionExpression, diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 8deb92640c1..9d0b71fe794 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -8,7 +8,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 1be657ba762..a9b44e63f36 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.kernel.objective import minimize from pyomo.common.collections import OrderedSet from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available diff --git a/pyomo/solver/__init__.py b/pyomo/contrib/solver/__init__.py similarity index 100% rename from pyomo/solver/__init__.py rename to pyomo/contrib/solver/__init__.py diff --git a/pyomo/solver/base.py b/pyomo/contrib/solver/base.py similarity index 99% rename from pyomo/solver/base.py rename to pyomo/contrib/solver/base.py index d7f4adabf56..69ad921b182 100644 --- a/pyomo/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -26,9 +26,9 @@ from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.config import UpdateConfig -from pyomo.solver.util import get_objective -from pyomo.solver.results import ( +from pyomo.contrib.solver.config import UpdateConfig +from pyomo.contrib.solver.util import get_objective +from pyomo.contrib.solver.results import ( Results, legacy_solver_status_map, legacy_termination_condition_map, diff --git a/pyomo/solver/config.py b/pyomo/contrib/solver/config.py similarity index 100% rename from pyomo/solver/config.py rename to pyomo/contrib/solver/config.py diff --git a/pyomo/solver/factory.py b/pyomo/contrib/solver/factory.py similarity index 94% rename from pyomo/solver/factory.py rename to pyomo/contrib/solver/factory.py index 23a66acd9cb..fa3e2611667 100644 --- a/pyomo/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -12,7 +12,7 @@ from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory -from pyomo.solver.base import LegacySolverInterface +from pyomo.contrib.solver.base import LegacySolverInterface class SolverFactoryClass(Factory): diff --git a/pyomo/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py similarity index 97% rename from pyomo/solver/ipopt.py rename to pyomo/contrib/solver/ipopt.py index 51f48ec4881..cb70938a074 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -24,16 +24,16 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn -from pyomo.solver.base import SolverBase, SymbolMap -from pyomo.solver.config import SolverConfig -from pyomo.solver.factory import SolverFactory -from pyomo.solver.results import ( +from pyomo.contrib.solver.base import SolverBase, SymbolMap +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.factory import SolverFactory +from pyomo.contrib.solver.results import ( Results, TerminationCondition, SolutionStatus, parse_sol_file, ) -from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader +from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions @@ -278,7 +278,7 @@ def solve(self, model, **kwds): if config.threads: logger.log( logging.WARNING, - msg=f"The `threads` option was specified, but but is not used by {self.__class__}.", + msg=f"The `threads` option was specified, but this is not used by {self.__class__}.", ) results = ipoptResults() with TempfileManager.new_context() as tempfile: diff --git a/pyomo/solver/plugins.py b/pyomo/contrib/solver/plugins.py similarity index 100% rename from pyomo/solver/plugins.py rename to pyomo/contrib/solver/plugins.py diff --git a/pyomo/solver/results.py b/pyomo/contrib/solver/results.py similarity index 99% rename from pyomo/solver/results.py rename to pyomo/contrib/solver/results.py index e99db52073b..c24053e6358 100644 --- a/pyomo/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -31,7 +31,7 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) -from pyomo.solver.solution import SolutionLoaderBase +from pyomo.contrib.solver.solution import SolutionLoaderBase from pyomo.repn.plugins.nl_writer import NLWriterInfo diff --git a/pyomo/solver/solution.py b/pyomo/contrib/solver/solution.py similarity index 100% rename from pyomo/solver/solution.py rename to pyomo/contrib/solver/solution.py diff --git a/pyomo/solver/tests/__init__.py b/pyomo/contrib/solver/tests/__init__.py similarity index 100% rename from pyomo/solver/tests/__init__.py rename to pyomo/contrib/solver/tests/__init__.py diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py similarity index 94% rename from pyomo/solver/tests/solvers/test_ipopt.py rename to pyomo/contrib/solver/tests/solvers/test_ipopt.py index d9fccbb84fc..c1aecba05fc 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -13,8 +13,8 @@ import pyomo.environ as pyo from pyomo.common.fileutils import ExecutableData from pyomo.common.config import ConfigDict -from pyomo.solver.ipopt import ipoptConfig -from pyomo.solver.factory import SolverFactory +from pyomo.contrib.solver.ipopt import ipoptConfig +from pyomo.contrib.solver.factory import SolverFactory from pyomo.common import unittest diff --git a/pyomo/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py similarity index 100% rename from pyomo/solver/tests/unit/test_base.py rename to pyomo/contrib/solver/tests/unit/test_base.py diff --git a/pyomo/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py similarity index 96% rename from pyomo/solver/tests/unit/test_config.py rename to pyomo/contrib/solver/tests/unit/test_config.py index c705c7cb8ac..1051825f4e5 100644 --- a/pyomo/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver.config import SolverConfig, BranchAndBoundConfig +from pyomo.contrib.solver.config import SolverConfig, BranchAndBoundConfig class TestSolverConfig(unittest.TestCase): diff --git a/pyomo/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py similarity index 100% rename from pyomo/solver/tests/unit/test_results.py rename to pyomo/contrib/solver/tests/unit/test_results.py diff --git a/pyomo/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py similarity index 100% rename from pyomo/solver/tests/unit/test_solution.py rename to pyomo/contrib/solver/tests/unit/test_solution.py diff --git a/pyomo/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py similarity index 97% rename from pyomo/solver/tests/unit/test_util.py rename to pyomo/contrib/solver/tests/unit/test_util.py index 737a271d603..9bf92af72cf 100644 --- a/pyomo/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -11,7 +11,7 @@ from pyomo.common import unittest import pyomo.environ as pyo -from pyomo.solver.util import collect_vars_and_named_exprs, get_objective +from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective from typing import Callable from pyomo.common.gsl import find_GSL diff --git a/pyomo/solver/util.py b/pyomo/contrib/solver/util.py similarity index 99% rename from pyomo/solver/util.py rename to pyomo/contrib/solver/util.py index c0c99a00747..9f0c607a0db 100644 --- a/pyomo/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -22,8 +22,8 @@ from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant -from pyomo.solver.config import UpdateConfig -from pyomo.solver.results import TerminationCondition, SolutionStatus +from pyomo.contrib.solver.config import UpdateConfig +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus def get_objective(block): diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 2cd562edb2b..51c68449247 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,7 +30,6 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', - 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', From aa28193717ea2f93b920fe8e9104832a98f079b4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 15:31:42 -0700 Subject: [PATCH 0208/1178] Missed several imports --- pyomo/contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 2 +- pyomo/contrib/solver/tests/unit/test_results.py | 4 ++-- pyomo/contrib/solver/tests/unit/test_solution.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 52f4992b37b..15c3fcb2058 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,7 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer -from pyomo.solver import results +from pyomo.contrib.solver import results def main(plot=True, n_points=200): diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index b501f8d3dd3..71690b7aa0e 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver import base +from pyomo.contrib.solver import base class TestSolverBase(unittest.TestCase): diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 0c0b4bb18db..e7d02751f7d 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -11,8 +11,8 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict -from pyomo.solver import results -from pyomo.solver import solution +from pyomo.contrib.solver import results +from pyomo.contrib.solver import solution import pyomo.environ as pyo from pyomo.core.base.var import ScalarVar diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index f4c33a60c84..dc53f1e4543 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver import solution +from pyomo.contrib.solver import solution class TestPersistentSolverBase(unittest.TestCase): From f0d9685b006d7ab40cceeeaaf2a118e778add56d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 15:52:34 -0700 Subject: [PATCH 0209/1178] Missing init files --- pyomo/contrib/solver/tests/solvers/__init__.py | 11 +++++++++++ pyomo/contrib/solver/tests/unit/__init__.py | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 pyomo/contrib/solver/tests/solvers/__init__.py create mode 100644 pyomo/contrib/solver/tests/unit/__init__.py diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py new file mode 100644 index 00000000000..9320e403e95 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py new file mode 100644 index 00000000000..9320e403e95 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + From 7a1a0d50c7c83204faf343d6524eaede0fad8e1f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 16:26:09 -0700 Subject: [PATCH 0210/1178] Black --- pyomo/contrib/solver/tests/solvers/__init__.py | 1 - pyomo/contrib/solver/tests/unit/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/solver/tests/solvers/__init__.py +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/solver/tests/unit/__init__.py +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - From 4087d1adb3cfea55c6e85547ce0697b5b860601d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 15 Jan 2024 23:49:20 -0700 Subject: [PATCH 0211/1178] solver refactor: various updates --- pyomo/contrib/solver/base.py | 61 ++++++++++---------------------- pyomo/contrib/solver/config.py | 15 ++++---- pyomo/contrib/solver/ipopt.py | 57 +++++++++++++---------------- pyomo/contrib/solver/results.py | 21 +++-------- pyomo/contrib/solver/solution.py | 50 ++------------------------ pyomo/repn/plugins/nl_writer.py | 2 +- 6 files changed, 58 insertions(+), 148 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 69ad921b182..961187179f2 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,6 +14,8 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os +from .config import SolverConfig + from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -49,6 +51,11 @@ class SolverBase(abc.ABC): - is_persistent: Set to false for all direct solvers. """ + CONFIG = SolverConfig() + + def __init__(self, **kwds) -> None: + self.config = self.CONFIG(value=kwds) + # # Support "with" statements. Forgetting to call deactivate # on Plugins is a common source of memory leaks @@ -146,19 +153,6 @@ def version(self) -> Tuple: A tuple representing the version """ - @property - @abc.abstractmethod - def config(self): - """ - An object for configuring solve options. - - Returns - ------- - SolverConfig - An object for configuring pyomo solve options such as the time limit. - These options are mostly independent of the solver. - """ - def is_persistent(self): """ Returns @@ -187,7 +181,7 @@ def is_persistent(self): """ return True - def load_vars( + def _load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: """ @@ -199,12 +193,12 @@ def load_vars( A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution to all primal variables will be loaded. """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + for v, val in self._get_primals(vars_to_load=vars_to_load).items(): v.set_value(val, skip_validation=True) StaleFlagManager.mark_all_as_stale(delayed=True) @abc.abstractmethod - def get_primals( + def _get_primals( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: """ @@ -224,7 +218,7 @@ def get_primals( '{0} does not support the get_primals method'.format(type(self)) ) - def get_duals( + def _get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: """ @@ -245,26 +239,7 @@ def get_duals( '{0} does not support the get_duals method'.format(type(self)) ) - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Parameters - ---------- - cons_to_load: list - A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slack values - """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) - - def get_reduced_costs( + def _get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: """ @@ -296,6 +271,12 @@ def set_instance(self, model): Set an instance of the model """ + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + """ + Set current objective for the model + """ + @abc.abstractmethod def add_variables(self, variables: List[_GeneralVarData]): """ @@ -344,12 +325,6 @@ def remove_block(self, block: _BlockData): Remove a block from the model """ - @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): - """ - Set current objective for the model - """ - @abc.abstractmethod def update_variables(self, variables: List[_GeneralVarData]): """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index ef0114ba439..738338d3718 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -16,7 +16,9 @@ ConfigValue, NonNegativeFloat, NonNegativeInt, + ADVANCED_OPTION, ) +from pyomo.common.timing import HierarchicalTimer class SolverConfig(ConfigDict): @@ -72,12 +74,11 @@ def __init__( description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", ), ) - self.report_timing: bool = self.declare( - 'report_timing', + self.timer: HierarchicalTimer = self.declare( + 'timer', ConfigValue( - domain=bool, - default=False, - description="If True, timing information will be printed at the end of a solve call.", + default=None, + description="A HierarchicalTimer.", ), ) self.threads: Optional[int] = self.declare( @@ -133,9 +134,6 @@ def __init__( self.abs_gap: Optional[float] = self.declare( 'abs_gap', ConfigValue(domain=NonNegativeFloat) ) - self.relax_integrality: bool = self.declare( - 'relax_integrality', ConfigValue(domain=bool, default=False) - ) class UpdateConfig(ConfigDict): @@ -283,6 +281,7 @@ def __init__( ConfigValue( domain=bool, default=True, + visibility=ADVANCED_OPTION, doc=""" This is an advanced option that should only be used in special circumstances. With the default setting of True, fixed variables will be treated like parameters. diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index cb70938a074..475ce6e6f0b 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -20,6 +20,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import Objective from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager @@ -84,6 +85,10 @@ def __init__( self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) + self.writer_config = self.declare( + 'writer_config', + ConfigValue(default=NLWriter.CONFIG()) + ) class ipoptResults(Results): @@ -183,10 +188,8 @@ class ipopt(SolverBase): CONFIG = ipoptConfig() def __init__(self, **kwds): - self._config = self.CONFIG(kwds) + super().__init__(**kwds) self._writer = NLWriter() - self._writer.config.skip_trivial_constraints = True - self._solver_options = self._config.solver_options def available(self): if self.config.executable.path() is None: @@ -206,26 +209,6 @@ def version(self): version = tuple(int(i) for i in version.split('.')) return version - @property - def writer(self): - return self._writer - - @property - def config(self): - return self._config - - @config.setter - def config(self, val): - self._config = val - - @property - def solver_options(self): - return self._solver_options - - @solver_options.setter - def solver_options(self, val: Dict): - self._solver_options = val - @property def symbol_map(self): return self._symbol_map @@ -250,14 +233,14 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd = [str(config.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') - if 'option_file_name' in self.solver_options: + if 'option_file_name' in config.solver_options: raise ValueError( 'Pyomo generates the ipopt options file as part of the solve method. ' 'Add all options to ipopt.config.solver_options instead.' ) - if config.time_limit is not None and 'max_cpu_time' not in self.solver_options: - self.solver_options['max_cpu_time'] = config.time_limit - for k, val in self.solver_options.items(): + if config.time_limit is not None and 'max_cpu_time' not in config.solver_options: + config.solver_options['max_cpu_time'] = config.time_limit + for k, val in config.solver_options.items(): if k in ipopt_command_line_options: cmd.append(str(k) + '=' + str(val)) return cmd @@ -271,15 +254,18 @@ def solve(self, model, **kwds): raise ipoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) - StaleFlagManager.mark_all_as_stale() # Update configuration options, based on keywords passed to solve - config: ipoptConfig = self.config(kwds.pop('options', {})) - config.set_value(kwds) + config: ipoptConfig = self.config(value=kwds) if config.threads: logger.log( logging.WARNING, msg=f"The `threads` option was specified, but this is not used by {self.__class__}.", ) + if config.timer is None: + timer = HierarchicalTimer() + else: + timer = config.timer + StaleFlagManager.mark_all_as_stale() results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.temp_dir is None: @@ -296,6 +282,8 @@ def solve(self, model, **kwds): with open(basename + '.nl', 'w') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: + timer.start('write_nl_file') + self._writer.config.set_value(config.writer_config) nl_info = self._writer.write( model, nl_file, @@ -303,6 +291,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + timer.stop('write_nl_file') # Get a copy of the environment to pass to the subprocess env = os.environ.copy() if nl_info.external_function_libraries: @@ -318,7 +307,7 @@ def solve(self, model, **kwds): # Write the opt_file, if there should be one; return a bool to say # whether or not we have one (so we can correctly build the command line) opt_file = self._write_options_file( - filename=basename, options=self.solver_options + filename=basename, options=config.solver_options ) # Call ipopt - passing the files via the subprocess cmd = self._create_command_line( @@ -343,6 +332,7 @@ def solve(self, model, **kwds): ) ) with TeeStream(*ostreams) as t: + timer.start('subprocess') process = subprocess.run( cmd, timeout=timeout, @@ -351,6 +341,7 @@ def solve(self, model, **kwds): stdout=t.STDOUT, stderr=t.STDERR, ) + timer.stop('subprocess') # This is the stuff we need to parse to get the iterations # and time iters, ipopt_time_nofunc, ipopt_time_func = self._parse_ipopt_output( @@ -362,7 +353,9 @@ def solve(self, model, **kwds): results.solution_loader = SolutionLoader(None, None, None, None) else: with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') results = self._parse_solution(sol_file, nl_info, results) + timer.stop('parse_sol') results.iteration_count = iters results.timing_info.no_function_solve_time = ipopt_time_nofunc results.timing_info.function_solve_time = ipopt_time_func @@ -427,8 +420,6 @@ def solve(self, model, **kwds): results.timing_info.wall_time = ( end_timestamp - start_timestamp ).total_seconds() - if config.report_timing: - results.report_timing() return results def _parse_ipopt_output(self, stream: io.StringIO): diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index c24053e6358..2f839580a43 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ import enum -from typing import Optional, Tuple, Dict, Any, Sequence, List +from typing import Optional, Tuple, Dict, Any, Sequence, List, Type from datetime import datetime import io @@ -21,6 +21,7 @@ NonNegativeInt, In, NonNegativeFloat, + ADVANCED_OPTION, ) from pyomo.common.errors import PyomoException from pyomo.core.base.var import _GeneralVarData @@ -224,7 +225,7 @@ def __init__( self.iteration_count: Optional[int] = self.declare( 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) ) - self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) + self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict(implicit=True)) self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue(domain=Datetime) @@ -235,20 +236,8 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - - def __str__(self): - s = '' - s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'solution_status: ' + str(self.solution_status) + '\n' - s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' - s += 'objective_bound: ' + str(self.objective_bound) - return s - - def report_timing(self): - print('Timing Information: ') - print('-' * 50) - self.timing_info.display() - print('-' * 50) + self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigDict(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) + self.solver_log: str = self.declare('solver_log', ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION)) class ResultsReader: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 068677ea580..4ec3f98cd08 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -95,27 +95,6 @@ def get_duals( """ raise NotImplementedError(f'{type(self)} does not support the get_duals method') - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Returns a dictionary mapping constraint to slack. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slacks - """ - raise NotImplementedError( - f'{type(self)} does not support the get_slacks method' - ) - def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: @@ -198,23 +177,6 @@ def get_duals( duals[c] = self._duals[c] return duals - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - if self._slacks is None: - raise RuntimeError( - 'Solution loader does not currently have valid slacks. Please ' - 'check the termination condition and ensure the solver returns slacks ' - 'for the given problem type.' - ) - if cons_to_load is None: - slacks = dict(self._slacks) - else: - slacks = {} - for c in cons_to_load: - slacks[c] = self._slacks[c] - return slacks - def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: @@ -244,25 +206,19 @@ def _assert_solution_still_valid(self): def get_primals(self, vars_to_load=None): self._assert_solution_still_valid() - return self._solver.get_primals(vars_to_load=vars_to_load) + return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: self._assert_solution_still_valid() - return self._solver.get_duals(cons_to_load=cons_to_load) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver.get_slacks(cons_to_load=cons_to_load) + return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: self._assert_solution_still_valid() - return self._solver.get_reduced_costs(vars_to_load=vars_to_load) + return self._solver._get_reduced_costs(vars_to_load=vars_to_load) def invalidate(self): self._valid = False diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 187d3176bb7..3b94963e858 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -214,7 +214,7 @@ class NLWriter(object): CONFIG.declare( 'skip_trivial_constraints', ConfigValue( - default=False, + default=True, domain=bool, description='Skip writing constraints whose body is constant', ), From e5c46edc0dbee83b2e75569ec0105cd750fe1e0e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:00:18 -0700 Subject: [PATCH 0212/1178] solver refactor: various updates --- pyomo/contrib/solver/results.py | 90 +++++++++++++++------------------ 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 2f839580a43..c7edc8c2f2e 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -257,10 +257,8 @@ def __init__(self) -> None: def parse_sol_file( sol_file: io.TextIOBase, nl_info: NLWriterInfo, - suffixes_to_read: Sequence[str], result: Results, ) -> Tuple[Results, SolFileData]: - suffixes_to_read = set(suffixes_to_read) sol_data = SolFileData() # @@ -368,9 +366,8 @@ def parse_sol_file( if result.solution_status != SolutionStatus.noSolution: for v, val in zip(nl_info.variables, variable_vals): sol_data.primals[id(v)] = (v, val) - if "dual" in suffixes_to_read: - for c, val in zip(nl_info.constraints, duals): - sol_data.duals[c] = val + for c, val in zip(nl_info.constraints, duals): + sol_data.duals[c] = val ### Read suffixes ### line = sol_file.readline() while line: @@ -400,51 +397,46 @@ def parse_sol_file( # tablen = int(line[4]) tabline = int(line[5]) suffix_name = sol_file.readline().strip() - if suffix_name in suffixes_to_read: - # ignore translation of the table number to string value for now, - # this information can be obtained from the solver documentation - for n in range(tabline): - sol_file.readline() - if kind == 0: # Var - sol_data.var_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - var_ndx = int(suf_line[0]) - var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = ( - var, - convert_function(suf_line[1]), - ) - elif kind == 1: # Con - sol_data.con_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - con_ndx = int(suf_line[0]) - con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function( - suf_line[1] - ) - elif kind == 2: # Obj - sol_data.obj_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - obj_ndx = int(suf_line[0]) - obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = ( - obj, - convert_function(suf_line[1]), - ) - elif kind == 3: # Prob - sol_data.problem_suffixes[suffix_name] = list() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - sol_data.problem_suffixes[suffix_name].append( - convert_function(suf_line[1]) - ) - else: - # do not store the suffix in the solution object + # ignore translation of the table number to string value for now, + # this information can be obtained from the solver documentation + for n in range(tabline): + sol_file.readline() + if kind == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() for cnt in range(nvalues): - sol_file.readline() + suf_line = sol_file.readline().split() + var_ndx = int(suf_line[0]) + var = nl_info.variables[var_ndx] + sol_data.var_suffixes[suffix_name][id(var)] = ( + var, + convert_function(suf_line[1]), + ) + elif kind == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + con_ndx = int(suf_line[0]) + con = nl_info.constraints[con_ndx] + sol_data.con_suffixes[suffix_name][con] = convert_function( + suf_line[1] + ) + elif kind == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + obj_ndx = int(suf_line[0]) + obj = nl_info.objectives[obj_ndx] + sol_data.obj_suffixes[suffix_name][id(obj)] = ( + obj, + convert_function(suf_line[1]), + ) + elif kind == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) line = sol_file.readline() return result, sol_data From dac6bef9459e38be840428005b59ab0947ffaaa2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:14:27 -0700 Subject: [PATCH 0213/1178] solver refactor: use caching in available and version --- pyomo/contrib/solver/ipopt.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 475ce6e6f0b..878fe7cb264 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -190,24 +190,31 @@ class ipopt(SolverBase): def __init__(self, **kwds): super().__init__(**kwds) self._writer = NLWriter() + self._available_cache = None + self._version_cache = None def available(self): - if self.config.executable.path() is None: - return self.Availability.NotFound - return self.Availability.FullLicense + if self._available_cache is None: + if self.config.executable.path() is None: + self._available_cache = self.Availability.NotFound + else: + self._available_cache = self.Availability.FullLicense + return self._available_cache def version(self): - results = subprocess.run( - [str(self.config.executable), '--version'], - timeout=1, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - ) - version = results.stdout.splitlines()[0] - version = version.split(' ')[1].strip() - version = tuple(int(i) for i in version.split('.')) - return version + if self._version_cache is None: + results = subprocess.run( + [str(self.config.executable), '--version'], + timeout=1, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1].strip() + version = tuple(int(i) for i in version.split('.')) + self._version_cache = version + return self._version_cache @property def symbol_map(self): From 4df0a8dd7f6518e12f24920d60b5e1fc10114d3f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:26:32 -0700 Subject: [PATCH 0214/1178] solver refactor: config updates --- pyomo/contrib/solver/base.py | 7 - pyomo/contrib/solver/config.py | 272 +++++++++++++++++++-------------- 2 files changed, 156 insertions(+), 123 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 961187179f2..216bf28ac4a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -258,13 +258,6 @@ def _get_reduced_costs( '{0} does not support the get_reduced_costs method'.format(type(self)) ) - @property - @abc.abstractmethod - def update_config(self) -> UpdateConfig: - """ - Updates the solver config - """ - @abc.abstractmethod def set_instance(self, model): """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 738338d3718..0a2478d44ff 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -21,122 +21,7 @@ from pyomo.common.timing import HierarchicalTimer -class SolverConfig(ConfigDict): - """ - Base config values for all solver interfaces - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.tee: bool = self.declare( - 'tee', - ConfigValue( - domain=bool, - default=False, - description="If True, the solver log prints to stdout.", - ), - ) - self.load_solution: bool = self.declare( - 'load_solution', - ConfigValue( - domain=bool, - default=True, - description="If True, the values of the primal variables will be loaded into the model.", - ), - ) - self.raise_exception_on_nonoptimal_result: bool = self.declare( - 'raise_exception_on_nonoptimal_result', - ConfigValue( - domain=bool, - default=True, - description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", - ), - ) - self.symbolic_solver_labels: bool = self.declare( - 'symbolic_solver_labels', - ConfigValue( - domain=bool, - default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", - ), - ) - self.timer: HierarchicalTimer = self.declare( - 'timer', - ConfigValue( - default=None, - description="A HierarchicalTimer.", - ), - ) - self.threads: Optional[int] = self.declare( - 'threads', - ConfigValue( - domain=NonNegativeInt, - description="Number of threads to be used by a solver.", - default=None, - ), - ) - self.time_limit: Optional[float] = self.declare( - 'time_limit', - ConfigValue( - domain=NonNegativeFloat, description="Time limit applied to the solver." - ), - ) - self.solver_options: ConfigDict = self.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - - -class BranchAndBoundConfig(SolverConfig): - """ - Attributes - ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.rel_gap: Optional[float] = self.declare( - 'rel_gap', ConfigValue(domain=NonNegativeFloat) - ) - self.abs_gap: Optional[float] = self.declare( - 'abs_gap', ConfigValue(domain=NonNegativeFloat) - ) - - -class UpdateConfig(ConfigDict): +class AutoUpdateConfig(ConfigDict): """ This is necessary for persistent solvers. @@ -294,3 +179,158 @@ def __init__( updating the values of fixed variables is much faster this way.""", ), ) + + +class SolverConfig(ConfigDict): + """ + Base config values for all solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.tee: bool = self.declare( + 'tee', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver log prints to stdout.", + ), + ) + self.load_solution: bool = self.declare( + 'load_solution', + ConfigValue( + domain=bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), + ) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue( + domain=bool, + default=True, + description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + ), + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', + ConfigValue( + domain=bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + ), + ) + self.timer: HierarchicalTimer = self.declare( + 'timer', + ConfigValue( + default=None, + description="A HierarchicalTimer.", + ), + ) + self.threads: Optional[int] = self.declare( + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + default=None, + ), + ) + self.time_limit: Optional[float] = self.declare( + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, description="Time limit applied to the solver." + ), + ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + + +class BranchAndBoundConfig(SolverConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.rel_gap: Optional[float] = self.declare( + 'rel_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.abs_gap: Optional[float] = self.declare( + 'abs_gap', ConfigValue(domain=NonNegativeFloat) + ) + + +class PersistentSolverConfig(SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + + +class PersistentBranchAndBoundConfig(BranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) From b94477913d92c5c346906bf0ca297862ac40baae Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:27:33 -0700 Subject: [PATCH 0215/1178] solver refactor: typo --- pyomo/contrib/solver/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 0a2478d44ff..8fe627cbcc1 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -313,7 +313,7 @@ def __init__( visibility=visibility, ) - self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) class PersistentBranchAndBoundConfig(BranchAndBoundConfig): @@ -333,4 +333,4 @@ def __init__( visibility=visibility, ) - self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) From fe41e220167ac95381bd5ef40852d1180cdb466f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:44:33 -0700 Subject: [PATCH 0216/1178] solver refactor: various fixes --- pyomo/contrib/solver/base.py | 1 - pyomo/contrib/solver/ipopt.py | 5 +++-- pyomo/contrib/solver/results.py | 2 +- pyomo/contrib/solver/util.py | 26 ++++++++------------------ 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 216bf28ac4a..56292859b1a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -28,7 +28,6 @@ from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.config import UpdateConfig from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( Results, diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 878fe7cb264..5176291ab42 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -421,6 +421,9 @@ def solve(self, model, **kwds): remove_named_expressions=True, ) + results.solver_configuration = config + results.solver_log = ostreams[0].getvalue() + # Capture/record end-time / wall-time end_timestamp = datetime.datetime.now(datetime.timezone.utc) results.timing_info.start_timestamp = start_timestamp @@ -462,11 +465,9 @@ def _parse_ipopt_output(self, stream: io.StringIO): def _parse_solution( self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults ): - suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] res, sol_data = parse_sol_file( sol_file=instream, nl_info=nl_info, - suffixes_to_read=suffixes_to_read, result=result, ) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index c7edc8c2f2e..e63aa351f64 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -236,7 +236,7 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigDict(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) + self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigValue(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) self.solver_log: str = self.declare('solver_log', ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION)) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index 9f0c607a0db..727d9c354e2 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -22,7 +22,6 @@ from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant -from pyomo.contrib.solver.config import UpdateConfig from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus @@ -154,7 +153,6 @@ def __init__(self, only_child_vars=False): ) # maps constraint to list of tuples (named_expr, named_expr.expr) self._external_functions = ComponentMap() self._obj_named_expressions = [] - self._update_config = UpdateConfig() self._referenced_variables = ( {} ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] @@ -163,18 +161,10 @@ def __init__(self, only_child_vars=False): self._expr_types = None self._only_child_vars = only_child_vars - @property - def update_config(self): - return self._update_config - - @update_config.setter - def update_config(self, val: UpdateConfig): - self._update_config = val - def set_instance(self, model): - saved_update_config = self.update_config + saved_config = self.config self.__init__(only_child_vars=self._only_child_vars) - self.update_config = saved_update_config + self.config = saved_config self._model = model self.add_block(model) if self._objective is None: @@ -249,7 +239,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): self._vars_referenced_by_con[con] = variables for v in variables: self._referenced_variables[id(v)][0][con] = None - if not self.update_config.treat_fixed_vars_as_params: + if not self.config.auto_updates.treat_fixed_vars_as_params: for v in fixed_vars: v.unfix() all_fixed_vars[id(v)] = v @@ -302,7 +292,7 @@ def set_objective(self, obj: _GeneralObjectiveData): self._vars_referenced_by_obj = variables for v in variables: self._referenced_variables[id(v)][2] = obj - if not self.update_config.treat_fixed_vars_as_params: + if not self.config.auto_updates.treat_fixed_vars_as_params: for v in fixed_vars: v.unfix() self._set_objective(obj) @@ -483,7 +473,7 @@ def update_params(self): def update(self, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() - config = self.update_config + config = self.config.auto_updates new_vars = [] old_vars = [] new_params = [] @@ -634,7 +624,7 @@ def update(self, timer: HierarchicalTimer = None): _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] if (fixed != v.fixed) or (fixed and (value != v.value)): vars_to_update.append(v) - if self.update_config.treat_fixed_vars_as_params: + if self.config.auto_updates.treat_fixed_vars_as_params: for c in self._referenced_variables[id(v)][0]: cons_to_remove_and_add[c] = None if self._referenced_variables[id(v)][2] is not None: @@ -670,13 +660,13 @@ def update(self, timer: HierarchicalTimer = None): break timer.stop('named expressions') timer.start('objective') - if self.update_config.check_for_new_objective: + if self.config.auto_updates.check_for_new_objective: pyomo_obj = get_objective(self._model) if pyomo_obj is not self._objective: need_to_set_objective = True else: pyomo_obj = self._objective - if self.update_config.update_objective: + if self.config.auto_updates.update_objective: if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: need_to_set_objective = True elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: From 3d32f4a1aa098e06d3193a3258164101599fffe0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 01:45:57 -0700 Subject: [PATCH 0217/1178] move sol reader to separate file --- pyomo/contrib/solver/ipopt.py | 2 +- pyomo/contrib/solver/results.py | 205 +--------------------------- pyomo/contrib/solver/sol_reader.py | 206 +++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 205 deletions(-) create mode 100644 pyomo/contrib/solver/sol_reader.py diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5176291ab42..c7a932eb883 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -32,8 +32,8 @@ Results, TerminationCondition, SolutionStatus, - parse_sol_file, ) +from .sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index e63aa351f64..3beb3aede81 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -10,9 +10,8 @@ # ___________________________________________________________________________ import enum -from typing import Optional, Tuple, Dict, Any, Sequence, List, Type +from typing import Optional, Tuple from datetime import datetime -import io from pyomo.common.config import ( ConfigDict, @@ -24,16 +23,12 @@ ADVANCED_OPTION, ) from pyomo.common.errors import PyomoException -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _ConstraintData -from pyomo.core.base.objective import _ObjectiveData from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) from pyomo.contrib.solver.solution import SolutionLoaderBase -from pyomo.repn.plugins.nl_writer import NLWriterInfo class SolverResultsError(PyomoException): @@ -244,204 +239,6 @@ class ResultsReader: pass -class SolFileData: - def __init__(self) -> None: - self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() - self.duals: Dict[_ConstraintData, float] = dict() - self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() - self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() - self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() - self.problem_suffixes: Dict[str, List[Any]] = dict() - - -def parse_sol_file( - sol_file: io.TextIOBase, - nl_info: NLWriterInfo, - result: Results, -) -> Tuple[Results, SolFileData]: - sol_data = SolFileData() - - # - # Some solvers (minto) do not write a message. We will assume - # all non-blank lines up the 'Options' line is the message. - # For backwards compatibility and general safety, we will parse all - # lines until "Options" appears. Anything before "Options" we will - # consider to be the solver message. - message = [] - for line in sol_file: - if not line: - break - line = line.strip() - if "Options" in line: - break - message.append(line) - message = '\n'.join(message) - # Once "Options" appears, we must now read the content under it. - model_objects = [] - if "Options" in line: - line = sol_file.readline() - number_of_options = int(line) - need_tolerance = False - if ( - number_of_options > 4 - ): # MRM: Entirely unclear why this is necessary, or if it even is - number_of_options -= 2 - need_tolerance = True - for i in range(number_of_options + 4): - line = sol_file.readline() - model_objects.append(int(line)) - if ( - need_tolerance - ): # MRM: Entirely unclear why this is necessary, or if it even is - line = sol_file.readline() - model_objects.append(float(line)) - else: - raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") - # Identify the total number of variables and constraints - number_of_cons = model_objects[number_of_options + 1] - number_of_vars = model_objects[number_of_options + 3] - assert number_of_cons == len(nl_info.constraints) - assert number_of_vars == len(nl_info.variables) - - duals = [float(sol_file.readline()) for i in range(number_of_cons)] - variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] - - # Parse the exit code line and capture it - exit_code = [0, 0] - line = sol_file.readline() - if line and ('objno' in line): - exit_code_line = line.split() - if len(exit_code_line) != 3: - raise SolverResultsError( - f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." - ) - exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] - else: - raise SolverResultsError( - f"ERROR READING `sol` FILE. Expected `objno`; received {line}." - ) - result.extra_info.solver_message = message.strip().replace('\n', '; ') - exit_code_message = '' - if (exit_code[1] >= 0) and (exit_code[1] <= 99): - result.solution_status = SolutionStatus.optimal - result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied - elif (exit_code[1] >= 100) and (exit_code[1] <= 199): - exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" - result.solution_status = SolutionStatus.feasible - result.termination_condition = TerminationCondition.error - elif (exit_code[1] >= 200) and (exit_code[1] <= 299): - exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" - result.solution_status = SolutionStatus.infeasible - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? - result.termination_condition = TerminationCondition.locallyInfeasible - elif (exit_code[1] >= 300) and (exit_code[1] <= 399): - exit_code_message = ( - "UNBOUNDED PROBLEM: the objective can be improved without limit!" - ) - result.solution_status = SolutionStatus.noSolution - result.termination_condition = TerminationCondition.unbounded - elif (exit_code[1] >= 400) and (exit_code[1] <= 499): - exit_code_message = ( - "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " - "was stopped by a limit that you set!" - ) - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? - result.solution_status = SolutionStatus.infeasible - result.termination_condition = TerminationCondition.iterationLimit - elif (exit_code[1] >= 500) and (exit_code[1] <= 599): - exit_code_message = ( - "FAILURE: the solver stopped by an error condition " - "in the solver routines!" - ) - result.termination_condition = TerminationCondition.error - - if result.extra_info.solver_message: - if exit_code_message: - result.extra_info.solver_message += '; ' + exit_code_message - else: - result.extra_info.solver_message = exit_code_message - - if result.solution_status != SolutionStatus.noSolution: - for v, val in zip(nl_info.variables, variable_vals): - sol_data.primals[id(v)] = (v, val) - for c, val in zip(nl_info.constraints, duals): - sol_data.duals[c] = val - ### Read suffixes ### - line = sol_file.readline() - while line: - line = line.strip() - if line == "": - continue - line = line.split() - # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes - if line[0] != 'suffix': - # We assume this is the start of a - # section like kestrel_option, which - # comes after all suffixes. - remaining = "" - line = sol_file.readline() - while line: - remaining += line.strip() + "; " - line = sol_file.readline() - result.extra_info.solver_message += remaining - break - unmasked_kind = int(line[1]) - kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob - convert_function = int - if (unmasked_kind & 4) == 4: - convert_function = float - nvalues = int(line[2]) - # namelen = int(line[3]) - # tablen = int(line[4]) - tabline = int(line[5]) - suffix_name = sol_file.readline().strip() - # ignore translation of the table number to string value for now, - # this information can be obtained from the solver documentation - for n in range(tabline): - sol_file.readline() - if kind == 0: # Var - sol_data.var_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - var_ndx = int(suf_line[0]) - var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = ( - var, - convert_function(suf_line[1]), - ) - elif kind == 1: # Con - sol_data.con_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - con_ndx = int(suf_line[0]) - con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function( - suf_line[1] - ) - elif kind == 2: # Obj - sol_data.obj_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - obj_ndx = int(suf_line[0]) - obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = ( - obj, - convert_function(suf_line[1]), - ) - elif kind == 3: # Prob - sol_data.problem_suffixes[suffix_name] = list() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - sol_data.problem_suffixes[suffix_name].append( - convert_function(suf_line[1]) - ) - line = sol_file.readline() - - return result, sol_data - - def parse_yaml(): pass diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py new file mode 100644 index 00000000000..f1fc7998179 --- /dev/null +++ b/pyomo/contrib/solver/sol_reader.py @@ -0,0 +1,206 @@ +from typing import Tuple, Dict, Any, List +import io + +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.objective import _ObjectiveData +from pyomo.repn.plugins.nl_writer import NLWriterInfo +from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition + + +class SolFileData: + def __init__(self) -> None: + self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() + self.duals: Dict[_ConstraintData, float] = dict() + self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() + self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() + self.problem_suffixes: Dict[str, List[Any]] = dict() + + +def parse_sol_file( + sol_file: io.TextIOBase, + nl_info: NLWriterInfo, + result: Results, +) -> Tuple[Results, SolFileData]: + sol_data = SolFileData() + + # + # Some solvers (minto) do not write a message. We will assume + # all non-blank lines up the 'Options' line is the message. + # For backwards compatibility and general safety, we will parse all + # lines until "Options" appears. Anything before "Options" we will + # consider to be the solver message. + message = [] + for line in sol_file: + if not line: + break + line = line.strip() + if "Options" in line: + break + message.append(line) + message = '\n'.join(message) + # Once "Options" appears, we must now read the content under it. + model_objects = [] + if "Options" in line: + line = sol_file.readline() + number_of_options = int(line) + need_tolerance = False + if ( + number_of_options > 4 + ): # MRM: Entirely unclear why this is necessary, or if it even is + number_of_options -= 2 + need_tolerance = True + for i in range(number_of_options + 4): + line = sol_file.readline() + model_objects.append(int(line)) + if ( + need_tolerance + ): # MRM: Entirely unclear why this is necessary, or if it even is + line = sol_file.readline() + model_objects.append(float(line)) + else: + raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") + # Identify the total number of variables and constraints + number_of_cons = model_objects[number_of_options + 1] + number_of_vars = model_objects[number_of_options + 3] + assert number_of_cons == len(nl_info.constraints) + assert number_of_vars == len(nl_info.variables) + + duals = [float(sol_file.readline()) for i in range(number_of_cons)] + variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] + + # Parse the exit code line and capture it + exit_code = [0, 0] + line = sol_file.readline() + if line and ('objno' in line): + exit_code_line = line.split() + if len(exit_code_line) != 3: + raise SolverResultsError( + f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." + ) + exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + else: + raise SolverResultsError( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) + result.extra_info.solver_message = message.strip().replace('\n', '; ') + exit_code_message = '' + if (exit_code[1] >= 0) and (exit_code[1] <= 99): + result.solution_status = SolutionStatus.optimal + result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif (exit_code[1] >= 100) and (exit_code[1] <= 199): + exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" + result.solution_status = SolutionStatus.feasible + result.termination_condition = TerminationCondition.error + elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" + result.solution_status = SolutionStatus.infeasible + # TODO: this is solver dependent + # But this was the way in the previous version - and has been fine thus far? + result.termination_condition = TerminationCondition.locallyInfeasible + elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + exit_code_message = ( + "UNBOUNDED PROBLEM: the objective can be improved without limit!" + ) + result.solution_status = SolutionStatus.noSolution + result.termination_condition = TerminationCondition.unbounded + elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + exit_code_message = ( + "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!" + ) + # TODO: this is solver dependent + # But this was the way in the previous version - and has been fine thus far? + result.solution_status = SolutionStatus.infeasible + result.termination_condition = TerminationCondition.iterationLimit + elif (exit_code[1] >= 500) and (exit_code[1] <= 599): + exit_code_message = ( + "FAILURE: the solver stopped by an error condition " + "in the solver routines!" + ) + result.termination_condition = TerminationCondition.error + + if result.extra_info.solver_message: + if exit_code_message: + result.extra_info.solver_message += '; ' + exit_code_message + else: + result.extra_info.solver_message = exit_code_message + + if result.solution_status != SolutionStatus.noSolution: + for v, val in zip(nl_info.variables, variable_vals): + sol_data.primals[id(v)] = (v, val) + for c, val in zip(nl_info.constraints, duals): + sol_data.duals[c] = val + ### Read suffixes ### + line = sol_file.readline() + while line: + line = line.strip() + if line == "": + continue + line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes + if line[0] != 'suffix': + # We assume this is the start of a + # section like kestrel_option, which + # comes after all suffixes. + remaining = "" + line = sol_file.readline() + while line: + remaining += line.strip() + "; " + line = sol_file.readline() + result.extra_info.solver_message += remaining + break + unmasked_kind = int(line[1]) + kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob + convert_function = int + if (unmasked_kind & 4) == 4: + convert_function = float + nvalues = int(line[2]) + # namelen = int(line[3]) + # tablen = int(line[4]) + tabline = int(line[5]) + suffix_name = sol_file.readline().strip() + # ignore translation of the table number to string value for now, + # this information can be obtained from the solver documentation + for n in range(tabline): + sol_file.readline() + if kind == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + var_ndx = int(suf_line[0]) + var = nl_info.variables[var_ndx] + sol_data.var_suffixes[suffix_name][id(var)] = ( + var, + convert_function(suf_line[1]), + ) + elif kind == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + con_ndx = int(suf_line[0]) + con = nl_info.constraints[con_ndx] + sol_data.con_suffixes[suffix_name][con] = convert_function( + suf_line[1] + ) + elif kind == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + obj_ndx = int(suf_line[0]) + obj = nl_info.objectives[obj_ndx] + sol_data.obj_suffixes[suffix_name][id(obj)] = ( + obj, + convert_function(suf_line[1]), + ) + elif kind == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) + line = sol_file.readline() + + return result, sol_data From 6c5d83c5ff23c175229e511088d2fe8b569daa48 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 01:59:52 -0700 Subject: [PATCH 0218/1178] remove symbol map when it is not necessary --- pyomo/contrib/solver/base.py | 6 ++---- pyomo/contrib/solver/ipopt.py | 13 +------------ 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 56292859b1a..962d35582a1 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -27,6 +27,7 @@ from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap +from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( @@ -425,10 +426,7 @@ def solve( legacy_soln.gap = None symbol_map = SymbolMap() - symbol_map.byObject = dict(symbol_map.byObject) - symbol_map.bySymbol = dict(symbol_map.bySymbol) - symbol_map.aliases = dict(symbol_map.aliases) - symbol_map.default_labeler = symbol_map.default_labeler + symbol_map.default_labeler = NumericLabeler('x') model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index c7a932eb883..5e84fd8796c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -22,10 +22,9 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import Objective -from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn -from pyomo.contrib.solver.base import SolverBase, SymbolMap +from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import ( @@ -216,10 +215,6 @@ def version(self): self._version_cache = version return self._version_cache - @property - def symbol_map(self): - return self._symbol_map - def _write_options_file(self, filename: str, options: Mapping): # First we need to determine if we even need to create a file. # If options is empty, then we return False @@ -305,12 +300,6 @@ def solve(self, model, **kwds): if env.get('AMPLFUNC'): nl_info.external_function_libraries.append(env.get('AMPLFUNC')) env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) - symbol_map = self._symbol_map = SymbolMap() - labeler = NumericLabeler('component') - for v in nl_info.variables: - symbol_map.getSymbol(v, labeler) - for c in nl_info.constraints: - symbol_map.getSymbol(c, labeler) # Write the opt_file, if there should be one; return a bool to say # whether or not we have one (so we can correctly build the command line) opt_file = self._write_options_file( From c800776eebfdebbe1988be3d9dec15bf9763103c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 02:12:06 -0700 Subject: [PATCH 0219/1178] reorg --- pyomo/contrib/solver/ipopt.py | 25 +------------------------ pyomo/contrib/solver/sol_reader.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5e84fd8796c..23464e40cb2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -392,11 +392,7 @@ def solve(self, model, **kwds): if results.solution_status in { SolutionStatus.feasible, SolutionStatus.optimal, - } and len( - list( - model.component_data_objects(Objective, descend_into=True, active=True) - ) - ): + } and len(nl_info.objectives) > 0: if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: @@ -476,14 +472,6 @@ def _parse_solution( if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) - if len(nl_info.eliminated_vars) > 0: - sub_map = {k: v[1] for k, v in sol_data.primals.items()} - for v, v_expr in nl_info.eliminated_vars: - val = evaluate_ampl_repn(v_expr, sub_map) - v_id = id(v) - sub_map[v_id] = val - sol_data.primals[v_id] = (v, val) - res.solution_loader = SolutionLoader( primals=sol_data.primals, duals=sol_data.duals, @@ -492,14 +480,3 @@ def _parse_solution( ) return res - - -def evaluate_ampl_repn(repn: AMPLRepn, sub_map): - assert not repn.nonlinear - assert repn.nl is None - val = repn.const - if repn.linear is not None: - for v_id, v_coef in repn.linear.items(): - val += v_coef * sub_map[v_id] - val *= repn.mult - return val diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index f1fc7998179..93fb6d39da3 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -6,6 +6,18 @@ from pyomo.core.base.objective import _ObjectiveData from pyomo.repn.plugins.nl_writer import NLWriterInfo from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition +from pyomo.repn.plugins.nl_writer import AMPLRepn + + +def evaluate_ampl_repn(repn: AMPLRepn, sub_map): + assert not repn.nonlinear + assert repn.nl is None + val = repn.const + if repn.linear is not None: + for v_id, v_coef in repn.linear.items(): + val += v_coef * sub_map[v_id] + val *= repn.mult + return val class SolFileData: @@ -203,4 +215,12 @@ def parse_sol_file( ) line = sol_file.readline() + if len(nl_info.eliminated_vars) > 0: + sub_map = {k: v[1] for k, v in sol_data.primals.items()} + for v, v_expr in nl_info.eliminated_vars: + val = evaluate_ampl_repn(v_expr, sub_map) + v_id = id(v) + sub_map[v_id] = val + sol_data.primals[v_id] = (v, val) + return result, sol_data From 952e8e77bca81c65d914fc35fc2a43fdea69a9cc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 07:44:41 -0700 Subject: [PATCH 0220/1178] solver refactor: various fixes --- pyomo/contrib/solver/ipopt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 23464e40cb2..2705abec7c6 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -71,9 +71,6 @@ def __init__( self.executable = self.declare( 'executable', ConfigValue(default=Executable('ipopt')) ) - self.save_solver_io: bool = self.declare( - 'save_solver_io', ConfigValue(domain=bool, default=False) - ) # TODO: Add in a deprecation here for keepfiles self.temp_dir: str = self.declare( 'temp_dir', ConfigValue(domain=str, default=None) From e256ab19c56375bbe760fc1f81db3f8e57a27b6b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 08:48:00 -0700 Subject: [PATCH 0221/1178] make skip_trivial_constraints True by default --- pyomo/repn/plugins/nl_writer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3b94963e858..897b906c181 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -338,6 +338,9 @@ def __call__(self, model, filename, solver_capability, io_options): config.scale_model = False config.linear_presolve = False + # just for backwards compatibility + config.skip_trivial_constraints = False + if config.symbolic_solver_labels: _open = lambda fname: open(fname, 'w') else: From 5b7919ec202c1b16ae0ab8adf6f479eac5b2fb36 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Jan 2024 09:24:35 -0700 Subject: [PATCH 0222/1178] Apply black; convert doc -> description for ConfigDicts --- pyomo/contrib/solver/config.py | 254 ++++++++++++++--------------- pyomo/contrib/solver/ipopt.py | 26 ++- pyomo/contrib/solver/results.py | 17 +- pyomo/contrib/solver/sol_reader.py | 4 +- 4 files changed, 153 insertions(+), 148 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 8fe627cbcc1..84b1c2d2c87 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -21,6 +21,117 @@ from pyomo.common.timing import HierarchicalTimer +class SolverConfig(ConfigDict): + """ + Base config values for all solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.tee: bool = self.declare( + 'tee', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver log prints to stdout.", + ), + ) + self.load_solution: bool = self.declare( + 'load_solution', + ConfigValue( + domain=bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), + ) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue( + domain=bool, + default=True, + description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + ), + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', + ConfigValue( + domain=bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + ), + ) + self.timer: HierarchicalTimer = self.declare( + 'timer', ConfigValue(default=None, description="A HierarchicalTimer.") + ) + self.threads: Optional[int] = self.declare( + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + default=None, + ), + ) + self.time_limit: Optional[float] = self.declare( + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, description="Time limit applied to the solver." + ), + ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + + +class BranchAndBoundConfig(SolverConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.rel_gap: Optional[float] = self.declare( + 'rel_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.abs_gap: Optional[float] = self.declare( + 'abs_gap', ConfigValue(domain=NonNegativeFloat) + ) + + class AutoUpdateConfig(ConfigDict): """ This is necessary for persistent solvers. @@ -59,7 +170,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old constraints will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_constraints() and opt.remove_constraints() or when you are certain constraints are not being @@ -71,7 +182,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old variables will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_variables() and opt.remove_variables() or when you are certain variables are not being added to / @@ -83,7 +194,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old parameters will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_params() and opt.remove_params() or when you are certain parameters are not being added to / @@ -95,7 +206,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old objectives will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.set_objective() or when you are certain objectives are not being added to / removed from the model.""", @@ -106,7 +217,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to existing constraints will not be automatically detected on subsequent solves. This includes changes to the lower, body, and upper attributes of constraints. Use False only when manually updating the solver with @@ -119,7 +230,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to existing variables will not be automatically detected on subsequent solves. This includes changes to the lb, ub, domain, and fixed attributes of variables. Use False only when manually updating the solver with @@ -131,7 +242,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to parameter values will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.update_params() or when you are certain parameters are not being modified.""", @@ -142,7 +253,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to Expressions will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.remove_constraints() and opt.add_constraints() or when you are certain @@ -154,7 +265,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to objectives will not be automatically detected on subsequent solves. This includes the expr and sense attributes of objectives. Use False only when manually updating the solver with opt.set_objective() or when you are @@ -167,7 +278,7 @@ def __init__( domain=bool, default=True, visibility=ADVANCED_OPTION, - doc=""" + description=""" This is an advanced option that should only be used in special circumstances. With the default setting of True, fixed variables will be treated like parameters. This means that z == x*y will be linear if x or y is fixed and the constraint @@ -181,121 +292,6 @@ def __init__( ) -class SolverConfig(ConfigDict): - """ - Base config values for all solver interfaces - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.tee: bool = self.declare( - 'tee', - ConfigValue( - domain=bool, - default=False, - description="If True, the solver log prints to stdout.", - ), - ) - self.load_solution: bool = self.declare( - 'load_solution', - ConfigValue( - domain=bool, - default=True, - description="If True, the values of the primal variables will be loaded into the model.", - ), - ) - self.raise_exception_on_nonoptimal_result: bool = self.declare( - 'raise_exception_on_nonoptimal_result', - ConfigValue( - domain=bool, - default=True, - description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", - ), - ) - self.symbolic_solver_labels: bool = self.declare( - 'symbolic_solver_labels', - ConfigValue( - domain=bool, - default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", - ), - ) - self.timer: HierarchicalTimer = self.declare( - 'timer', - ConfigValue( - default=None, - description="A HierarchicalTimer.", - ), - ) - self.threads: Optional[int] = self.declare( - 'threads', - ConfigValue( - domain=NonNegativeInt, - description="Number of threads to be used by a solver.", - default=None, - ), - ) - self.time_limit: Optional[float] = self.declare( - 'time_limit', - ConfigValue( - domain=NonNegativeFloat, description="Time limit applied to the solver." - ), - ) - self.solver_options: ConfigDict = self.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - - -class BranchAndBoundConfig(SolverConfig): - """ - Attributes - ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.rel_gap: Optional[float] = self.declare( - 'rel_gap', ConfigValue(domain=NonNegativeFloat) - ) - self.abs_gap: Optional[float] = self.declare( - 'abs_gap', ConfigValue(domain=NonNegativeFloat) - ) - - class PersistentSolverConfig(SolverConfig): def __init__( self, @@ -313,7 +309,9 @@ def __init__( visibility=visibility, ) - self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare( + 'auto_updates', AutoUpdateConfig() + ) class PersistentBranchAndBoundConfig(BranchAndBoundConfig): @@ -333,4 +331,6 @@ def __init__( visibility=visibility, ) - self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare( + 'auto_updates', AutoUpdateConfig() + ) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 2705abec7c6..63dca0af0d9 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -27,11 +27,7 @@ from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory -from pyomo.contrib.solver.results import ( - Results, - TerminationCondition, - SolutionStatus, -) +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from .sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.common.tee import TeeStream @@ -82,8 +78,7 @@ def __init__( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) self.writer_config = self.declare( - 'writer_config', - ConfigValue(default=NLWriter.CONFIG()) + 'writer_config', ConfigValue(default=NLWriter.CONFIG()) ) @@ -237,7 +232,10 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo 'Pyomo generates the ipopt options file as part of the solve method. ' 'Add all options to ipopt.config.solver_options instead.' ) - if config.time_limit is not None and 'max_cpu_time' not in config.solver_options: + if ( + config.time_limit is not None + and 'max_cpu_time' not in config.solver_options + ): config.solver_options['max_cpu_time'] = config.time_limit for k, val in config.solver_options.items(): if k in ipopt_command_line_options: @@ -386,10 +384,10 @@ def solve(self, model, **kwds): ): model.rc.update(results.solution_loader.get_reduced_costs()) - if results.solution_status in { - SolutionStatus.feasible, - SolutionStatus.optimal, - } and len(nl_info.objectives) > 0: + if ( + results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal} + and len(nl_info.objectives) > 0 + ): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: @@ -448,9 +446,7 @@ def _parse_solution( self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults ): res, sol_data = parse_sol_file( - sol_file=instream, - nl_info=nl_info, - result=result, + sol_file=instream, nl_info=nl_info, result=result ) if res.solution_status == SolutionStatus.noSolution: diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 3beb3aede81..e21adcc35cc 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -220,7 +220,9 @@ def __init__( self.iteration_count: Optional[int] = self.declare( 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) ) - self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict(implicit=True)) + self.timing_info: ConfigDict = self.declare( + 'timing_info', ConfigDict(implicit=True) + ) self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue(domain=Datetime) @@ -231,8 +233,17 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigValue(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) - self.solver_log: str = self.declare('solver_log', ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION)) + self.solver_configuration: ConfigDict = self.declare( + 'solver_configuration', + ConfigValue( + description="A copy of the config object used in the solve", + visibility=ADVANCED_OPTION, + ), + ) + self.solver_log: str = self.declare( + 'solver_log', + ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), + ) class ResultsReader: diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 93fb6d39da3..92761246241 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -31,9 +31,7 @@ def __init__(self) -> None: def parse_sol_file( - sol_file: io.TextIOBase, - nl_info: NLWriterInfo, - result: Results, + sol_file: io.TextIOBase, nl_info: NLWriterInfo, result: Results ) -> Tuple[Results, SolFileData]: sol_data = SolFileData() From 78c08d09c8396ccc85b480116c698568b8c088b4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 10:06:20 -0700 Subject: [PATCH 0223/1178] restore changes to appsi --- pyomo/contrib/appsi/__init__.py | 1 + pyomo/contrib/appsi/base.py | 1695 +++++++++++++++++ pyomo/contrib/appsi/build.py | 6 +- .../contrib/appsi/examples/getting_started.py | 14 +- .../appsi/examples/tests/test_examples.py | 15 +- pyomo/contrib/appsi/fbbt.py | 23 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 90 +- pyomo/contrib/appsi/solvers/cplex.py | 90 +- pyomo/contrib/appsi/solvers/gurobi.py | 149 +- pyomo/contrib/appsi/solvers/highs.py | 139 +- pyomo/contrib/appsi/solvers/ipopt.py | 86 +- .../solvers/tests/test_gurobi_persistent.py | 33 +- .../solvers/tests/test_highs_persistent.py | 11 +- .../solvers/tests/test_ipopt_persistent.py | 6 +- .../solvers/tests/test_persistent_solvers.py | 431 ++--- .../solvers/tests/test_wntr_persistent.py | 97 +- pyomo/contrib/appsi/solvers/wntr.py | 32 +- pyomo/contrib/appsi/tests/test_base.py | 91 + pyomo/contrib/appsi/tests/test_interval.py | 4 +- pyomo/contrib/appsi/utils/__init__.py | 2 + .../utils/collect_vars_and_named_exprs.py | 50 + pyomo/contrib/appsi/utils/get_objective.py | 12 + pyomo/contrib/appsi/utils/tests/__init__.py | 0 .../test_collect_vars_and_named_exprs.py | 56 + pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/lp_writer.py | 22 +- pyomo/contrib/appsi/writers/nl_writer.py | 33 +- .../appsi/writers/tests/test_nl_writer.py | 2 +- 29 files changed, 2493 insertions(+), 701 deletions(-) create mode 100644 pyomo/contrib/appsi/base.py create mode 100644 pyomo/contrib/appsi/tests/test_base.py create mode 100644 pyomo/contrib/appsi/utils/__init__.py create mode 100644 pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py create mode 100644 pyomo/contrib/appsi/utils/get_objective.py create mode 100644 pyomo/contrib/appsi/utils/tests/__init__.py create mode 100644 pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py diff --git a/pyomo/contrib/appsi/__init__.py b/pyomo/contrib/appsi/__init__.py index 0134a96f363..df3ba212448 100644 --- a/pyomo/contrib/appsi/__init__.py +++ b/pyomo/contrib/appsi/__init__.py @@ -1,3 +1,4 @@ +from . import base from . import solvers from . import writers from . import fbbt diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py new file mode 100644 index 00000000000..e6186eeedd2 --- /dev/null +++ b/pyomo/contrib/appsi/base.py @@ -0,0 +1,1695 @@ +import abc +import enum +from typing import ( + Sequence, + Dict, + Optional, + Mapping, + NoReturn, + List, + Tuple, + MutableMapping, +) +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from .utils.get_objective import get_objective +from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat +from pyomo.common.errors import ApplicationError +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +import os +from pyomo.opt.results.results_ import SolverResults as LegacySolverResults +from pyomo.opt.results.solution import ( + Solution as LegacySolution, + SolutionStatus as LegacySolutionStatus, +) +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) +from pyomo.core.kernel.objective import minimize +from pyomo.core.base import SymbolMap +import weakref +from .cmodel import cmodel, cmodel_available +from pyomo.core.staleflag import StaleFlagManager +from pyomo.core.expr.numvalue import NumericConstant + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + + unknown = 0 + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + + maxTimeLimit = 1 + """The solver exited due to a time limit""" + + maxIterations = 2 + """The solver exited due to an iteration limit """ + + objectiveLimit = 3 + """The solver exited due to an objective limit""" + + minStepLength = 4 + """The solver exited due to a minimum step length""" + + optimal = 5 + """The solver exited with the optimal solution""" + + unbounded = 8 + """The solver exited because the problem is unbounded""" + + infeasible = 9 + """The solver exited because the problem is infeasible""" + + infeasibleOrUnbounded = 10 + """The solver exited because the problem is either infeasible or unbounded""" + + error = 11 + """The solver exited due to an error""" + + interrupted = 12 + """The solver exited because it was interrupted""" + + licensingProblems = 13 + """The solver exited due to licensing problems""" + + +class SolverConfig(ConfigDict): + """ + Attributes + ---------- + time_limit: float + Time limit for the solver + stream_solver: bool + If True, then the solver log goes to stdout + load_solution: bool + If False, then the values of the primal variables will not be + loaded into the model + symbolic_solver_labels: bool + If True, the names given to the solver will reflect the names + of the pyomo components. Cannot be changed after set_instance + is called. + report_timing: bool + If True, then some timing information will be printed at the + end of the solve. + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(SolverConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) + self.declare('stream_solver', ConfigValue(domain=bool)) + self.declare('load_solution', ConfigValue(domain=bool)) + self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) + self.declare('report_timing', ConfigValue(domain=bool)) + + self.time_limit: Optional[float] = None + self.stream_solver: bool = False + self.load_solution: bool = True + self.symbolic_solver_labels: bool = False + self.report_timing: bool = False + + +class MIPSolverConfig(SolverConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MIPSolverConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) + self.declare('relax_integrality', ConfigValue(domain=bool)) + + self.mip_gap: Optional[float] = None + self.relax_integrality: bool = False + + +class SolutionLoaderBase(abc.ABC): + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to var value. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution value should be retrieved. If vars_to_load is None, + then the values for all variables will be retrieved. + + Returns + ------- + primals: ComponentMap + Maps variables to solution values + """ + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to dual value. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all + constraints will be retrieved. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError(f'{type(self)} does not support the get_duals method') + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to slack. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slacks + """ + raise NotImplementedError( + f'{type(self)} does not support the get_slacks method' + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to reduced cost. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the + reduced costs for all variables will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variables to reduced costs + """ + raise NotImplementedError( + f'{type(self)} does not support the get_reduced_costs method' + ) + + +class SolutionLoader(SolutionLoaderBase): + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + slacks: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + slacks: dict + maps Constraint to slack value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._slacks = slacks + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = dict() + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._slacks is None: + raise RuntimeError( + 'Solution loader does not currently have valid slacks. Please ' + 'check the termination condition and ensure the solver returns slacks ' + 'for the given problem type.' + ) + if cons_to_load is None: + slacks = dict(self._slacks) + else: + slacks = dict() + for c in cons_to_load: + slacks[c] = self._slacks[c] + return slacks + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + +class Results(object): + """ + Attributes + ---------- + termination_condition: TerminationCondition + The reason the solver exited. This is a member of the + TerminationCondition enum. + best_feasible_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + best_objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + + Here is an example workflow: + + >>> import pyomo.environ as pe + >>> from pyomo.contrib import appsi + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var() + >>> m.obj = pe.Objective(expr=m.x**2) + >>> opt = appsi.solvers.Ipopt() + >>> opt.config.load_solution = False + >>> results = opt.solve(m) #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.optimal: #doctest:+SKIP + ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars() #doctest:+SKIP + ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP + ... elif results.best_feasible_objective is not None: #doctest:+SKIP + ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP + ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP + ... elif results.termination_condition in {appsi.base.TerminationCondition.maxIterations, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP + ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP + ... else: #doctest:+SKIP + ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP + """ + + def __init__(self): + self.solution_loader: SolutionLoaderBase = SolutionLoader( + None, None, None, None + ) + self.termination_condition: TerminationCondition = TerminationCondition.unknown + self.best_feasible_objective: Optional[float] = None + self.best_objective_bound: Optional[float] = None + + def __str__(self): + s = '' + s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' + s += 'best_objective_bound: ' + str(self.best_objective_bound) + return s + + +class UpdateConfig(ConfigDict): + """ + Attributes + ---------- + check_for_new_or_removed_constraints: bool + check_for_new_or_removed_vars: bool + check_for_new_or_removed_params: bool + update_constraints: bool + update_vars: bool + update_params: bool + update_named_expressions: bool + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + if doc is None: + doc = 'Configuration options to detect changes in model between solves' + super(UpdateConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare( + 'check_for_new_or_removed_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old constraints will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_constraints() + and opt.remove_constraints() or when you are certain constraints are not being + added to/removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old variables will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_variables() and + opt.remove_variables() or when you are certain variables are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old parameters will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_params() and + opt.remove_params() or when you are certain parameters are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old objectives will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.set_objective() or + when you are certain objectives are not being added to / removed from the model.""", + ), + ) + self.declare( + 'update_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing constraints will not be automatically detected on + subsequent solves. This includes changes to the lower, body, and upper attributes of + constraints. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain constraints + are not being modified.""", + ), + ) + self.declare( + 'update_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing variables will not be automatically detected on + subsequent solves. This includes changes to the lb, ub, domain, and fixed + attributes of variables. Use False only when manually updating the solver with + opt.update_variables() or when you are certain variables are not being modified.""", + ), + ) + self.declare( + 'update_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to parameter values will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.update_params() or when you are certain parameters are not being modified.""", + ), + ) + self.declare( + 'update_named_expressions', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to Expressions will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain + Expressions are not being modified.""", + ), + ) + self.declare( + 'update_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to objectives will not be automatically detected on + subsequent solves. This includes the expr and sense attributes of objectives. Use + False only when manually updating the solver with opt.set_objective() or when you are + certain objectives are not being modified.""", + ), + ) + self.declare( + 'treat_fixed_vars_as_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + This is an advanced option that should only be used in special circumstances. + With the default setting of True, fixed variables will be treated like parameters. + This means that z == x*y will be linear if x or y is fixed and the constraint + can be written to an LP file. If the value of the fixed variable gets changed, we have + to completely reprocess all constraints using that variable. If + treat_fixed_vars_as_params is False, then constraints will be processed as if fixed + variables are not fixed, and the solver will be told the variable is fixed. This means + z == x*y could not be written to an LP file even if x and/or y is fixed. However, + updating the values of fixed variables is much faster this way.""", + ), + ) + + self.check_for_new_or_removed_constraints: bool = True + self.check_for_new_or_removed_vars: bool = True + self.check_for_new_or_removed_params: bool = True + self.check_for_new_objective: bool = True + self.update_constraints: bool = True + self.update_vars: bool = True + self.update_params: bool = True + self.update_named_expressions: bool = True + self.update_objective: bool = True + self.treat_fixed_vars_as_params: bool = True + + +class Solver(abc.ABC): + class Availability(enum.IntEnum): + NotFound = 0 + BadVersion = -1 + BadLicense = -2 + FullLicense = 1 + LimitedLicense = 2 + NeedsCompiledExtension = -3 + + def __bool__(self): + return self._value_ > 0 + + def __format__(self, format_spec): + # We want general formatting of this Enum to return the + # formatted string value and not the int (which is the + # default implementation from IntEnum) + return format(self.name, format_spec) + + def __str__(self): + # Note: Python 3.11 changed the core enums so that the + # "mixin" type for standard enums overrides the behavior + # specified in __format__. We will override str() here to + # preserve the previous behavior + return self.name + + @abc.abstractmethod + def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + """ + Solve a Pyomo model. + + Parameters + ---------- + model: _BlockData + The Pyomo model to be solved + timer: HierarchicalTimer + An option timer for reporting timing + + Returns + ------- + results: Results + A results object + """ + pass + + @abc.abstractmethod + def available(self): + """Test if the solver is available on this system. + + Nominally, this will return True if the solver interface is + valid and can be used to solve problems and False if it cannot. + + Note that for licensed solvers there are a number of "levels" of + available: depending on the license, the solver may be available + with limitations on problem size or runtime (e.g., 'demo' + vs. 'community' vs. 'full'). In these cases, the solver may + return a subclass of enum.IntEnum, with members that resolve to + True if the solver is available (possibly with limitations). + The Enum may also have multiple members that all resolve to + False indicating the reason why the interface is not available + (not found, bad license, unsupported version, etc). + + Returns + ------- + available: Solver.Availability + An enum that indicates "how available" the solver is. + Note that the enum can be cast to bool, which will + be True if the solver is runable at all and False + otherwise. + """ + pass + + @abc.abstractmethod + def version(self) -> Tuple: + """ + Returns + ------- + version: tuple + A tuple representing the version + """ + + @property + @abc.abstractmethod + def config(self): + """ + An object for configuring solve options. + + Returns + ------- + SolverConfig + An object for configuring pyomo solve options such as the time limit. + These options are mostly independent of the solver. + """ + pass + + @property + @abc.abstractmethod + def symbol_map(self): + pass + + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ + return False + + +class PersistentSolver(Solver): + def is_persistent(self): + return True + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slack values + """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) + + @property + @abc.abstractmethod + def update_config(self) -> UpdateConfig: + pass + + @abc.abstractmethod + def set_instance(self, model): + pass + + @abc.abstractmethod + def add_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def add_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def add_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def remove_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def remove_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def remove_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + pass + + @abc.abstractmethod + def update_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def update_params(self): + pass + + +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver: PersistentSolver): + self._solver = solver + self._valid = True + + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_duals(cons_to_load=cons_to_load) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_slacks(cons_to_load=cons_to_load) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + self._assert_solution_still_valid() + return self._solver.get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False + + +""" +What can change in a pyomo model? +- variables added or removed +- constraints added or removed +- objective changed +- objective expr changed +- params added or removed +- variable modified + - lb + - ub + - fixed or unfixed + - domain + - value +- constraint modified + - lower + - upper + - body + - active or not +- named expressions modified + - expr +- param modified + - value + +Ideas: +- Consider explicitly handling deactivated constraints; favor deactivation over removal + and activation over addition + +Notes: +- variable bounds cannot be updated with mutable params; you must call update_variables +""" + + +class PersistentBase(abc.ABC): + def __init__(self, only_child_vars=False): + self._model = None + self._active_constraints = dict() # maps constraint to (lower, body, upper) + self._vars = dict() # maps var id to (var, lb, ub, fixed, domain, value) + self._params = dict() # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + dict() + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = list() + self._update_config = UpdateConfig() + self._referenced_variables = ( + dict() + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = dict() + self._vars_referenced_by_obj = list() + self._expr_types = None + self.use_extensions = False + self._only_child_vars = only_child_vars + + @property + def update_config(self): + return self._update_config + + @update_config.setter + def update_config(self, val: UpdateConfig): + self._update_config = val + + def set_instance(self, model): + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.update_config = saved_update_config + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [dict(), dict(), None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = dict() + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = dict() + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = dict() + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(con.body, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = list() + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = list() + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = list() + self._set_objective(obj) + + def add_block(self, block): + param_dict = dict() + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + if self._only_child_vars: + self.add_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects(Var, descend_into=True) + ).values() + ) + ) + self.add_constraints( + [ + con + for con in block.component_data_objects( + Constraint, descend_into=True, active=True + ) + ] + ) + self.add_sos_constraints( + [ + con + for con in block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ] + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + [ + con + for con in block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ] + ) + self.remove_sos_constraints( + [ + con + for con in block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ] + ) + if self._only_child_vars: + self.remove_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects( + ctype=Var, descend_into=True + ) + ).values() + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.update_config + new_vars = list() + old_vars = list() + new_params = list() + old_params = list() + new_cons = list() + old_cons = list() + old_sos = list() + new_sos = list() + current_vars_dict = dict() + current_cons_dict = dict() + current_sos_dict = dict() + timer.start('vars') + if self._only_child_vars and ( + config.check_for_new_or_removed_vars or config.update_vars + ): + current_vars_dict = { + id(v): v + for v in self._model.component_data_objects(Var, descend_into=True) + } + for v_id, v in current_vars_dict.items(): + if v_id not in self._vars: + new_vars.append(v) + for v_id, v_tuple in self._vars.items(): + if v_id not in current_vars_dict: + old_vars.append(v_tuple[0]) + elif config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = dict() + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = dict() + need_to_set_objective = False + if config.update_constraints: + cons_to_update = list() + sos_to_update = list() + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if self._only_child_vars and config.update_vars: + vars_to_check = list() + for v_id, v in current_vars_dict.items(): + if v_id not in new_vars_set: + vars_to_check.append(v) + elif config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = list() + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if (fixed != v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.update_config.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = list() + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.update_config.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.update_config.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') + + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.optimal: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.maxIterations: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.optimal: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.infeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + TerminationCondition.unknown: LegacySolutionStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.minStepLength: LegacySolutionStatus.error, + TerminationCondition.optimal: LegacySolutionStatus.optimal, + TerminationCondition.unbounded: LegacySolutionStatus.unbounded, + TerminationCondition.infeasible: LegacySolutionStatus.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, + TerminationCondition.error: LegacySolutionStatus.error, + TerminationCondition.interrupted: LegacySolutionStatus.error, + TerminationCondition.licensingProblems: LegacySolutionStatus.error, +} + + +class LegacySolverInterface(object): + def solve( + self, + model: _BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + ): + original_config = self.config + self.config = self.config() + self.config.stream_solver = tee + self.config.load_solution = load_solutions + self.config.symbolic_solver_labels = symbolic_solver_labels + self.config.time_limit = timelimit + self.config.report_timing = report_timing + if solver_io is not None: + raise NotImplementedError('Still working on this') + if suffixes is not None: + raise NotImplementedError('Still working on this') + if logfile is not None: + raise NotImplementedError('Still working on this') + if 'keepfiles' in self.config: + self.config.keepfiles = keepfiles + if solnfile is not None: + if 'filename' in self.config: + filename = os.path.splitext(solnfile)[0] + self.config.filename = filename + original_options = self.options + if options is not None: + self.options = options + + results: Results = super(LegacySolverInterface, self).solve(model) + + legacy_results = LegacySolverResults() + legacy_soln = LegacySolution() + legacy_results.solver.status = legacy_solver_status_map[ + results.termination_condition + ] + legacy_results.solver.termination_condition = legacy_termination_condition_map[ + results.termination_condition + ] + legacy_soln.status = legacy_solution_status_map[results.termination_condition] + legacy_results.solver.termination_message = str(results.termination_condition) + + obj = get_objective(model) + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.best_objective_bound + legacy_results.problem.upper_bound = results.best_feasible_objective + else: + legacy_results.problem.upper_bound = results.best_objective_bound + legacy_results.problem.lower_bound = results.best_feasible_objective + if ( + results.best_feasible_objective is not None + and results.best_objective_bound is not None + ): + legacy_soln.gap = abs( + results.best_feasible_objective - results.best_objective_bound + ) + else: + legacy_soln.gap = None + + symbol_map = SymbolMap() + symbol_map.byObject = dict(self.symbol_map.byObject) + symbol_map.bySymbol = dict(self.symbol_map.bySymbol) + symbol_map.aliases = dict(self.symbol_map.aliases) + symbol_map.default_labeler = self.symbol_map.default_labeler + model.solutions.add_symbol_map(symbol_map) + legacy_results._smap_id = id(symbol_map) + + delete_legacy_soln = True + if load_solutions: + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + model.dual[c] = val + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + model.slack[c] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + model.rc[v] = val + elif results.best_feasible_objective is not None: + delete_legacy_soln = False + for v, val in results.solution_loader.get_primals().items(): + legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + symbol = symbol_map.getSymbol(c) + if symbol in legacy_soln.constraint: + legacy_soln.constraint[symbol]['Slack'] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + legacy_soln.variable['Rc'] = val + + legacy_results.solution.insert(legacy_soln) + if delete_legacy_soln: + legacy_results.solution.delete(0) + + self.config = original_config + self.options = original_options + + return legacy_results + + def available(self, exception_flag=True): + ans = super(LegacySolverInterface, self).available() + if exception_flag and not ans: + raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') + return bool(ans) + + def license_is_valid(self) -> bool: + """Test if the solver license is valid on this system. + + Note that this method is included for compatibility with the + legacy SolverFactory interface. Unlicensed or open source + solvers will return True by definition. Licensed solvers will + return True if a valid license is found. + + Returns + ------- + available: bool + True if the solver license is valid. Otherwise, False. + + """ + return bool(self.available()) + + @property + def options(self): + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + return getattr(self, solver_name + '_options') + raise NotImplementedError('Could not find the correct options') + + @options.setter + def options(self, val): + found = False + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + setattr(self, solver_name + '_options', val) + found = True + if not found: + raise NotImplementedError('Could not find the correct options') + + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + pass + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverInterface, cls): + pass + + LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index c00da19eae8..2c8d02dd3ac 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -9,9 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import errno import shutil -import stat import glob import os import sys @@ -81,7 +79,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super().run() + super(appsi_build_ext, self).run() if not self.inplace: library = glob.glob("build/*/appsi_cmodel.*")[0] target = os.path.join( @@ -118,7 +116,7 @@ def run(self): pybind11.setup_helpers.MACOS = original_pybind11_setup_helpers_macos -class AppsiBuilder: +class AppsiBuilder(object): def __call__(self, parallel): return build_appsi() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 15c3fcb2058..de22d28e0a4 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,6 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer -from pyomo.contrib.solver import results def main(plot=True, n_points=200): @@ -17,7 +16,7 @@ def main(plot=True, n_points=200): m.c1 = pe.Constraint(expr=m.y >= (m.x + 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= (m.x - m.p) ** 2) - opt = appsi.solvers.Ipopt() # create an APPSI solver interface + opt = appsi.solvers.Cplex() # create an APPSI solver interface opt.config.load_solution = False # modify the config options # change how automatic updates are handled opt.update_config.check_for_new_or_removed_vars = False @@ -25,18 +24,15 @@ def main(plot=True, n_points=200): # write a for loop to vary the value of parameter p from 1 to 10 p_values = [float(i) for i in np.linspace(1, 10, n_points)] - obj_values = [] - x_values = [] + obj_values = list() + x_values = list() timer = HierarchicalTimer() # create a timer for some basic profiling timer.start('p loop') for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert ( - res.termination_condition - == results.TerminationCondition.convergenceCriteriaSatisfied - ) - obj_values.append(res.incumbent_objective) + assert res.termination_condition == appsi.base.TerminationCondition.optimal + obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) timer.stop('p loop') diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index 7c577366c41..d2c88224a7d 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,17 +1,18 @@ from pyomo.contrib.appsi.examples import getting_started -from pyomo.common import unittest -from pyomo.common.dependencies import attempt_import +import pyomo.common.unittest as unittest +import pyomo.environ as pe from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib import appsi -numpy, numpy_available = attempt_import('numpy') - @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') -@unittest.skipUnless(numpy_available, 'numpy is not available') class TestExamples(unittest.TestCase): def test_getting_started(self): - opt = appsi.solvers.Ipopt() + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + opt = appsi.solvers.Cplex() if not opt.available(): - raise unittest.SkipTest('ipopt is not available') + raise unittest.SkipTest('cplex is not available') getting_started.main(plot=False, n_points=10) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index ccbb3819554..92a0e0c8cbc 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.appsi.base import PersistentBase from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -11,9 +11,10 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize +from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import _BlockData from pyomo.core.base import SymbolMap, TextLabeler +from pyomo.common.errors import InfeasibleConstraintException class IntervalConfig(ConfigDict): @@ -34,7 +35,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(IntervalConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -59,16 +60,16 @@ def __init__( ) -class IntervalTightener(PersistentSolverUtils): +class IntervalTightener(PersistentBase): def __init__(self): - super().__init__() + super(IntervalTightener, self).__init__() self._config = IntervalConfig() self._cmodel = None - self._var_map = {} - self._con_map = {} - self._param_map = {} - self._rvar_map = {} - self._rcon_map = {} + self._var_map = dict() + self._con_map = dict() + self._param_map = dict() + self._rvar_map = dict() + self._rcon_map = dict() self._pyomo_expr_types = cmodel.PyomoExprTypes() self._symbolic_solver_labels: bool = False self._symbol_map = SymbolMap() @@ -253,7 +254,7 @@ def _update_pyomo_var_bounds(self): self._vars[v_id] = (_v, _lb, cv_ub, _fixed, _domain, _value) def _deactivate_satisfied_cons(self): - cons_to_deactivate = [] + cons_to_deactivate = list() if self.config.deactivate_satisfied_constraints: for c, cc in self._con_map.items(): if not cc.active: diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index ebccba09ab2..5333158239e 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.contrib.solver.factory import SolverFactory +from .base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 141c6de57bd..a3aae2a9213 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,16 +1,20 @@ -import logging -import math -import subprocess -import sys -from typing import Optional, Sequence, Dict, List, Mapping - - from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + SolverConfig, + PersistentSolutionLoader, +) from pyomo.contrib.appsi.writers import LPWriter from pyomo.common.log import LogStream +import logging +import subprocess from pyomo.core.kernel.objective import minimize, maximize +import math from pyomo.common.collections import ComponentMap +from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -18,14 +22,12 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream +import sys +from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) @@ -40,7 +42,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(CbcConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -61,15 +63,15 @@ def __init__( self.log_level = logging.INFO -class Cbc(PersistentSolverBase): +class Cbc(PersistentSolver): def __init__(self, only_child_vars=False): self._config = CbcConfig() - self._solver_options = {} + self._solver_options = dict() self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = {} - self._primal_sol = {} - self._reduced_costs = {} + self._dual_sol = dict() + self._primal_sol = dict() + self._reduced_costs = dict() self._last_results_object: Optional[Results] = None def available(self): @@ -230,19 +232,17 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif 'unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif termination_line.startswith('stopped on time'): results.termination_condition = TerminationCondition.maxTimeLimit obj_val = float(termination_line.split()[-1]) elif termination_line.startswith('stopped on iterations'): - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations obj_val = float(termination_line.split()[-1]) else: results.termination_condition = TerminationCondition.unknown @@ -261,9 +261,9 @@ def _parse_soln(self): first_var_line = ndx last_var_line = len(all_lines) - 1 - self._dual_sol = {} - self._primal_sol = {} - self._reduced_costs = {} + self._dual_sol = dict() + self._primal_sol = dict() + self._reduced_costs = dict() symbol_map = self._writer.symbol_map @@ -307,30 +307,26 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition == TerminationCondition.optimal and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: - results.incumbent_objective = obj_val - elif ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): + results.best_feasible_objective = obj_val + elif results.termination_condition == TerminationCondition.optimal: if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: - results.incumbent_objective = obj_val + results.best_feasible_objective = obj_val elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) return results @@ -366,7 +362,7 @@ def _check_and_escape_options(): yield tmp_k, tmp_v cmd = [str(config.executable)] - action_options = [] + action_options = list() if config.time_limit is not None: cmd.extend(['-sec', str(config.time_limit)]) cmd.extend(['-timeMode', 'elapsed']) @@ -387,7 +383,7 @@ def _check_and_escape_options(): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -407,24 +403,24 @@ def _check_and_escape_options(): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.incumbent_objective = None + results.best_feasible_objective = None else: timer.start('parse solution') results = self._parse_soln() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.incumbent_objective = None - results.objective_bound = None + results.best_feasible_objective = None + results.best_objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -435,7 +431,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.incumbent_objective is None + or self._last_results_object.best_feasible_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' @@ -455,7 +451,7 @@ def get_duals(self, cons_to_load=None): if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -473,7 +469,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 6f02ac12eb1..f03bee6ecc5 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,34 +1,35 @@ -import logging -import math -import sys -import time -from typing import Optional, Sequence, Dict, List, Mapping - - from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentSolutionLoader, +) from pyomo.contrib.appsi.writers import LPWriter -from pyomo.common.log import LogStream +import logging +import math from pyomo.common.collections import ComponentMap +from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer +import sys +import time +from pyomo.common.log import LogStream from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import BranchAndBoundConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) -class CplexConfig(BranchAndBoundConfig): +class CplexConfig(MIPSolverConfig): def __init__( self, description=None, @@ -37,7 +38,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(CplexConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -58,17 +59,17 @@ def __init__( class CplexResults(Results): def __init__(self, solver): - super().__init__() - self.timing_info.wall_time = None + super(CplexResults, self).__init__() + self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) -class Cplex(PersistentSolverBase): +class Cplex(PersistentSolver): _available = None def __init__(self, only_child_vars=False): self._config = CplexConfig() - self._solver_options = {} + self._solver_options = dict() self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None self._last_results_object: Optional[CplexResults] = None @@ -244,7 +245,7 @@ def _apply_solver(self, timer: HierarchicalTimer): log_stream = LogStream( level=self.config.log_level, logger=self.config.solver_output_logger ) - if config.tee: + if config.stream_solver: def _process_stream(arg): sys.stdout.write(arg) @@ -263,8 +264,8 @@ def _process_stream(arg): if config.time_limit is not None: cplex_model.parameters.timelimit.set(config.time_limit) - if config.rel_gap is not None: - cplex_model.parameters.mip.tolerances.mipgap.set(config.rel_gap) + if config.mip_gap is not None: + cplex_model.parameters.mip.tolerances.mipgap.set(config.mip_gap) timer.start('cplex solve') t0 = time.time() @@ -279,46 +280,52 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): cpxprob = self._cplex_model results = CplexResults(solver=self) - results.timing_info.wall_time = solve_time + results.wallclock_time = solve_time status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status in [3, 103]: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif status in [10]: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status in [11, 25, 107, 131]: results.termination_condition = TerminationCondition.maxTimeLimit else: results.termination_condition = TerminationCondition.unknown if self._writer.get_active_objective() is None: - results.incumbent_objective = None - results.objective_bound = None + results.best_feasible_objective = None + results.best_objective_bound = None else: if cpxprob.solution.get_solution_type() != cpxprob.solution.type.none: if ( cpxprob.variables.get_num_binary() + cpxprob.variables.get_num_integer() ) == 0: - results.incumbent_objective = cpxprob.solution.get_objective_value() - results.objective_bound = cpxprob.solution.get_objective_value() + results.best_feasible_objective = ( + cpxprob.solution.get_objective_value() + ) + results.best_objective_bound = ( + cpxprob.solution.get_objective_value() + ) else: - results.incumbent_objective = cpxprob.solution.get_objective_value() - results.objective_bound = cpxprob.solution.MIP.get_best_objective() + results.best_feasible_objective = ( + cpxprob.solution.get_objective_value() + ) + results.best_objective_bound = ( + cpxprob.solution.MIP.get_best_objective() + ) else: - results.incumbent_objective = None + results.best_feasible_objective = None if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: @@ -326,13 +333,10 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'A feasible solution was not found, so no solution can be loades. ' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) else: - if ( - results.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition != TerminationCondition.optimal: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' @@ -396,7 +400,7 @@ def get_duals( con_names = self._cplex_model.linear_constraints.get_names() dual_values = self._cplex_model.solution.get_dual_values() else: - con_names = [] + con_names = list() for con in cons_to_load: orig_name = symbol_map.byObject[id(con)] if con.equality: @@ -408,7 +412,7 @@ def get_duals( con_names.append(orig_name + '_ub') dual_values = self._cplex_model.solution.get_dual_values(con_names) - res = {} + res = dict() for name, val in zip(con_names, dual_values): orig_name = name[:-3] if orig_name == 'obj_const_con': diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index a947c8d7d7d..a173c69abc6 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,9 +1,7 @@ from collections.abc import Iterable import logging import math -import sys from typing import List, Dict, Optional - from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import @@ -14,20 +12,24 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import Var, _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import BranchAndBoundConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader -from pyomo.contrib.solver.util import PersistentSolverUtils - +import sys logger = logging.getLogger(__name__) @@ -51,7 +53,7 @@ class DegreeError(PyomoException): pass -class GurobiConfig(BranchAndBoundConfig): +class GurobiConfig(MIPSolverConfig): def __init__( self, description=None, @@ -60,7 +62,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(GurobiConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -93,12 +95,12 @@ def get_primals(self, vars_to_load=None, solution_number=0): class GurobiResults(Results): def __init__(self, solver): - super().__init__() - self.timing_info.wall_time = None + super(GurobiResults, self).__init__() + self.wallclock_time = None self.solution_loader = GurobiSolutionLoader(solver=solver) -class _MutableLowerBound: +class _MutableLowerBound(object): def __init__(self, expr): self.var = None self.expr = expr @@ -107,7 +109,7 @@ def update(self): self.var.setAttr('lb', value(self.expr)) -class _MutableUpperBound: +class _MutableUpperBound(object): def __init__(self, expr): self.var = None self.expr = expr @@ -116,7 +118,7 @@ def update(self): self.var.setAttr('ub', value(self.expr)) -class _MutableLinearCoefficient: +class _MutableLinearCoefficient(object): def __init__(self): self.expr = None self.var = None @@ -127,7 +129,7 @@ def update(self): self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) -class _MutableRangeConstant: +class _MutableRangeConstant(object): def __init__(self): self.lhs_expr = None self.rhs_expr = None @@ -143,7 +145,7 @@ def update(self): slack.ub = rhs_val - lhs_val -class _MutableConstant: +class _MutableConstant(object): def __init__(self): self.expr = None self.con = None @@ -152,7 +154,7 @@ def update(self): self.con.rhs = value(self.expr) -class _MutableQuadraticConstraint: +class _MutableQuadraticConstraint(object): def __init__( self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs ): @@ -187,7 +189,7 @@ def get_updated_rhs(self): return value(self.constant.expr) -class _MutableObjective: +class _MutableObjective(object): def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant = constant @@ -215,14 +217,14 @@ def get_updated_expression(self): return gurobi_expr -class _MutableQuadraticCoefficient: +class _MutableQuadraticCoefficient(object): def __init__(self): self.expr = None self.var1 = None self.var2 = None -class Gurobi(PersistentSolverUtils, PersistentSolverBase): +class Gurobi(PersistentBase, PersistentSolver): """ Interface to Gurobi """ @@ -231,21 +233,21 @@ class Gurobi(PersistentSolverUtils, PersistentSolverBase): _num_instances = 0 def __init__(self, only_child_vars=False): - super().__init__(only_child_vars=only_child_vars) + super(Gurobi, self).__init__(only_child_vars=only_child_vars) self._num_instances += 1 self._config = GurobiConfig() - self._solver_options = {} + self._solver_options = dict() self._solver_model = None self._symbol_map = SymbolMap() self._labeler = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_con_to_pyomo_con_map = {} - self._pyomo_sos_to_solver_sos_map = {} + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() self._range_constraints = OrderedSet() - self._mutable_helpers = {} - self._mutable_bounds = {} - self._mutable_quadratic_helpers = {} + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._mutable_quadratic_helpers = dict() self._mutable_objective = None self._needs_updated = True self._callback = None @@ -351,7 +353,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -364,8 +366,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setParam('TimeLimit', config.time_limit) - if config.rel_gap is not None: - self._solver_model.setParam('MIPGap', config.rel_gap) + if config.mip_gap is not None: + self._solver_model.setParam('MIPGap', config.mip_gap) for key, option in options.items(): self._solver_model.setParam(key, option) @@ -446,12 +448,12 @@ def _process_domain_and_bounds( return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - var_names = [] - vtypes = [] - lbs = [] - ubs = [] - mutable_lbs = {} - mutable_ubs = {} + var_names = list() + vtypes = list() + lbs = list() + ubs = list() + mutable_lbs = dict() + mutable_ubs = dict() for ndx, var in enumerate(variables): varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds( @@ -499,6 +501,8 @@ def set_instance(self, model): ) self._reinit() self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() @@ -515,8 +519,8 @@ def set_instance(self, model): self.set_objective(None) def _get_expr_from_pyomo_expr(self, expr): - mutable_linear_coefficients = [] - mutable_quadratic_coefficients = [] + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() repn = generate_standard_repn(expr, quadratic=True, compute_values=False) degree = repn.polynomial_degree() @@ -526,7 +530,7 @@ def _get_expr_from_pyomo_expr(self, expr): ) if len(repn.linear_vars) > 0: - linear_coef_vals = [] + linear_coef_vals = list() for ndx, coef in enumerate(repn.linear_coefs): if not is_constant(coef): mutable_linear_coefficient = _MutableLinearCoefficient() @@ -820,8 +824,8 @@ def _set_objective(self, obj): sense = gurobipy.GRB.MINIMIZE gurobi_expr = 0 repn_constant = 0 - mutable_linear_coefficients = [] - mutable_quadratic_coefficients = [] + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() else: if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE @@ -865,16 +869,14 @@ def _postsolve(self, timer: HierarchicalTimer): status = gprob.Status results = GurobiResults(self) - results.timing_info.wall_time = gprob.Runtime + results.wallclock_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif status == grb.INFEASIBLE: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif status == grb.INF_OR_UNBD: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == grb.UNBOUNDED: @@ -882,9 +884,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == grb.CUTOFF: results.termination_condition = TerminationCondition.objectiveLimit elif status == grb.ITERATION_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status == grb.NODE_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status == grb.TIME_LIMIT: results.termination_condition = TerminationCondition.maxTimeLimit elif status == grb.SOLUTION_LIMIT: @@ -900,33 +902,30 @@ def _postsolve(self, timer: HierarchicalTimer): else: results.termination_condition = TerminationCondition.unknown - results.incumbent_objective = None - results.objective_bound = None + results.best_feasible_objective = None + results.best_objective_bound = None if self._objective is not None: try: - results.incumbent_objective = gprob.ObjVal + results.best_feasible_objective = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): - results.incumbent_objective = None + results.best_feasible_objective = None try: - results.objective_bound = gprob.ObjBound + results.best_objective_bound = gprob.ObjBound except (gurobipy.GurobiError, AttributeError): if self._objective.sense == minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf - if results.incumbent_objective is not None and not math.isfinite( - results.incumbent_objective + if results.best_feasible_objective is not None and not math.isfinite( + results.best_feasible_objective ): - results.incumbent_objective = None + results.best_feasible_objective = None timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if ( - results.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition != TerminationCondition.optimal: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' @@ -939,7 +938,7 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') @@ -1048,7 +1047,7 @@ def get_duals(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - dual = {} + dual = dict() if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() @@ -1091,7 +1090,7 @@ def get_slacks(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - slack = {} + slack = dict() gurobi_range_con_vars = OrderedSet(self._solver_model.getVars()) - OrderedSet( self._pyomo_var_to_solver_var_map.values() @@ -1141,7 +1140,7 @@ def get_slacks(self, cons_to_load=None): def update(self, timer: HierarchicalTimer = None): if self._needs_updated: self._update_gurobi_model() - super().update(timer=timer) + super(Gurobi, self).update(timer=timer) self._update_gurobi_model() def _update_gurobi_model(self): @@ -1197,8 +1196,8 @@ def set_linear_constraint_attr(self, con, attr, val): if attr in {'Sense', 'RHS', 'ConstrName'}: raise ValueError( 'Linear constraint attr {0} cannot be set with' - ' the set_linear_constraint_attr method. Please use' - ' the remove_constraint and add_constraint methods.'.format(attr) + + ' the set_linear_constraint_attr method. Please use' + + ' the remove_constraint and add_constraint methods.'.format(attr) ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True @@ -1226,8 +1225,8 @@ def set_var_attr(self, var, attr, val): if attr in {'LB', 'UB', 'VType', 'VarName'}: raise ValueError( 'Var attr {0} cannot be set with' - ' the set_var_attr method. Please use' - ' the update_var method.'.format(attr) + + ' the set_var_attr method. Please use' + + ' the update_var method.'.format(attr) ) if attr == 'Obj': raise ValueError( @@ -1385,7 +1384,7 @@ def set_callback(self, func=None): >>> _c = _add_cut(4) # this is an arbitrary choice >>> >>> opt = appsi.solvers.Gurobi() - >>> opt.config.tee = True + >>> opt.config.stream_solver = True >>> opt.set_instance(m) # doctest:+SKIP >>> opt.gurobi_options['PreCrush'] = 1 >>> opt.gurobi_options['LazyConstraints'] = 1 diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 1680831471c..3d498f9388e 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,7 +1,5 @@ import logging -import sys from typing import List, Dict, Optional - from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException @@ -18,13 +16,18 @@ from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import BranchAndBoundConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader -from pyomo.contrib.solver.util import PersistentSolverUtils +import sys logger = logging.getLogger(__name__) @@ -35,7 +38,7 @@ class DegreeError(PyomoException): pass -class HighsConfig(BranchAndBoundConfig): +class HighsConfig(MIPSolverConfig): def __init__( self, description=None, @@ -44,7 +47,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(HighsConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -64,11 +67,11 @@ def __init__( class HighsResults(Results): def __init__(self, solver): super().__init__() - self.timing_info.wall_time = None + self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) -class _MutableVarBounds: +class _MutableVarBounds(object): def __init__(self, lower_expr, upper_expr, pyomo_var_id, var_map, highs): self.pyomo_var_id = pyomo_var_id self.lower_expr = lower_expr @@ -83,7 +86,7 @@ def update(self): self.highs.changeColBounds(col_ndx, lb, ub) -class _MutableLinearCoefficient: +class _MutableLinearCoefficient(object): def __init__(self, pyomo_con, pyomo_var_id, con_map, var_map, expr, highs): self.expr = expr self.highs = highs @@ -98,7 +101,7 @@ def update(self): self.highs.changeCoeff(row_ndx, col_ndx, value(self.expr)) -class _MutableObjectiveCoefficient: +class _MutableObjectiveCoefficient(object): def __init__(self, pyomo_var_id, var_map, expr, highs): self.expr = expr self.highs = highs @@ -110,7 +113,7 @@ def update(self): self.highs.changeColCost(col_ndx, value(self.expr)) -class _MutableObjectiveOffset: +class _MutableObjectiveOffset(object): def __init__(self, expr, highs): self.expr = expr self.highs = highs @@ -119,7 +122,7 @@ def update(self): self.highs.changeObjectiveOffset(value(self.expr)) -class _MutableConstraintBounds: +class _MutableConstraintBounds(object): def __init__(self, lower_expr, upper_expr, pyomo_con, con_map, highs): self.lower_expr = lower_expr self.upper_expr = upper_expr @@ -134,7 +137,7 @@ def update(self): self.highs.changeRowBounds(row_ndx, lb, ub) -class Highs(PersistentSolverUtils, PersistentSolverBase): +class Highs(PersistentBase, PersistentSolver): """ Interface to HiGHS """ @@ -144,14 +147,14 @@ class Highs(PersistentSolverUtils, PersistentSolverBase): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = HighsConfig() - self._solver_options = {} + self._solver_options = dict() self._solver_model = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_con_to_pyomo_con_map = {} - self._mutable_helpers = {} - self._mutable_bounds = {} - self._objective_helpers = [] + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._objective_helpers = list() self._last_results_object: Optional[HighsResults] = None self._sol = None @@ -208,7 +211,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -219,8 +222,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setOptionValue('time_limit', config.time_limit) - if config.rel_gap is not None: - self._solver_model.setOptionValue('mip_rel_gap', config.rel_gap) + if config.mip_gap is not None: + self._solver_model.setOptionValue('mip_rel_gap', config.mip_gap) for key, option in options.items(): self._solver_model.setOptionValue(key, option) @@ -298,10 +301,10 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - lbs = [] - ubs = [] - indices = [] - vtypes = [] + lbs = list() + ubs = list() + indices = list() + vtypes = list() current_num_vars = len(self._pyomo_var_to_solver_var_map) for v in variables: @@ -348,12 +351,14 @@ def set_instance(self, model): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: with capture_output(output=t.STDOUT, capture_fd=True): self._reinit() self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() self._solver_model = highspy.Highs() self.add_block(model) @@ -365,11 +370,11 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() current_num_cons = len(self._pyomo_con_to_solver_con_map) - lbs = [] - ubs = [] - starts = [] - var_indices = [] - coef_values = [] + lbs = list() + ubs = list() + starts = list() + var_indices = list() + coef_values = list() for con in cons: repn = generate_standard_repn( @@ -395,7 +400,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): highs=self._solver_model, ) if con not in self._mutable_helpers: - self._mutable_helpers[con] = [] + self._mutable_helpers[con] = list() self._mutable_helpers[con].append(mutable_linear_coefficient) if coef_val == 0: continue @@ -450,7 +455,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = [] + indices_to_remove = list() for con in cons: con_ndx = self._pyomo_con_to_solver_con_map.pop(con) del self._solver_con_to_pyomo_con_map[con_ndx] @@ -460,7 +465,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): len(indices_to_remove), np.array(indices_to_remove) ) con_ndx = 0 - new_con_map = {} + new_con_map = dict() for c in self._pyomo_con_to_solver_con_map.keys(): new_con_map[c] = con_ndx con_ndx += 1 @@ -481,7 +486,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = [] + indices_to_remove = list() for v in variables: v_id = id(v) v_ndx = self._pyomo_var_to_solver_var_map.pop(v_id) @@ -492,7 +497,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): len(indices_to_remove), np.array(indices_to_remove) ) v_ndx = 0 - new_var_map = {} + new_var_map = dict() for v_id in self._pyomo_var_to_solver_var_map.keys(): new_var_map[v_id] = v_ndx v_ndx += 1 @@ -506,10 +511,10 @@ def _update_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices = [] - lbs = [] - ubs = [] - vtypes = [] + indices = list() + lbs = list() + ubs = list() + vtypes = list() for v in variables: v_id = id(v) @@ -550,7 +555,7 @@ def _set_objective(self, obj): n = len(self._pyomo_var_to_solver_var_map) indices = np.arange(n) costs = np.zeros(n, dtype=np.double) - self._objective_helpers = [] + self._objective_helpers = list() if obj is None: sense = highspy.ObjSense.kMinimize self._solver_model.changeObjectiveOffset(0) @@ -602,7 +607,7 @@ def _postsolve(self, timer: HierarchicalTimer): status = highs.getModelStatus() results = HighsResults(self) - results.timing_info.wall_time = highs.getRunTime() + results.wallclock_time = highs.getRunTime() if status == highspy.HighsModelStatus.kNotset: results.termination_condition = TerminationCondition.unknown @@ -619,11 +624,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif status == highspy.HighsModelStatus.kInfeasible: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == highspy.HighsModelStatus.kUnbounded: @@ -635,7 +638,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kTimeLimit: results.termination_condition = TerminationCondition.maxTimeLimit elif status == highspy.HighsModelStatus.kIterationLimit: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status == highspy.HighsModelStatus.kUnknown: results.termination_condition = TerminationCondition.unknown else: @@ -644,14 +647,11 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition == TerminationCondition.optimal: has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, - TerminationCondition.iterationLimit, + TerminationCondition.maxIterations, TerminationCondition.maxTimeLimit, }: if self._sol.value_valid: @@ -659,10 +659,7 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if ( - results.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition != TerminationCondition.optimal: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' @@ -675,23 +672,23 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') info = highs.getInfo() - results.objective_bound = None - results.incumbent_objective = None + results.best_objective_bound = None + results.best_feasible_objective = None if self._objective is not None: if has_feasible_solution: - results.incumbent_objective = info.objective_function_value + results.best_feasible_objective = info.objective_function_value if info.mip_node_count == -1: if has_feasible_solution: - results.objective_bound = info.objective_function_value + results.best_objective_bound = info.objective_function_value else: - results.objective_bound = None + results.best_objective_bound = None else: - results.objective_bound = info.mip_dual_bound + results.best_objective_bound = info.mip_dual_bound return results @@ -709,7 +706,7 @@ def get_primals(self, vars_to_load=None, solution_number=0): res = ComponentMap() if vars_to_load is None: - var_ids_to_load = [] + var_ids_to_load = list() for v, ref_info in self._referenced_variables.items(): using_cons, using_sos, using_obj = ref_info if using_cons or using_sos or (using_obj is not None): @@ -754,7 +751,7 @@ def get_duals(self, cons_to_load=None): 'check the termination condition.' ) - res = {} + res = dict() if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) @@ -773,7 +770,7 @@ def get_slacks(self, cons_to_load=None): 'check the termination condition.' ) - res = {} + res = dict() if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index ec59b827192..d38a836a2ac 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,20 +1,22 @@ -import math -import os -import sys -from typing import Dict -import logging -import subprocess - - from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + SolverConfig, + PersistentSolutionLoader, +) from pyomo.contrib.appsi.writers import NLWriter from pyomo.common.log import LogStream +import logging +import subprocess from pyomo.core.kernel.objective import minimize +import math from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions -from typing import Optional, Sequence, List, Mapping +from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -22,14 +24,13 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream +import sys +from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException +import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) @@ -44,7 +45,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(IpoptConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -125,13 +126,13 @@ def __init__( } -class Ipopt(PersistentSolverBase): +class Ipopt(PersistentSolver): def __init__(self, only_child_vars=False): self._config = IpoptConfig() - self._solver_options = {} + self._solver_options = dict() self._writer = NLWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = {} + self._dual_sol = dict() self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None @@ -296,20 +297,19 @@ def _parse_sol(self): solve_cons = self._writer.get_ordered_cons() results = Results() - with open(self._filename + '.sol', 'r') as f: - all_lines = list(f.readlines()) + f = open(self._filename + '.sol', 'r') + all_lines = list(f.readlines()) + f.close() termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif 'Problem may be infeasible' in termination_line: - results.termination_condition = TerminationCondition.locallyInfeasible + results.termination_condition = TerminationCondition.infeasible elif 'problem might be unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif 'Maximum Number of Iterations Exceeded' in termination_line: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif 'Maximum CPU Time Exceeded' in termination_line: results.termination_condition = TerminationCondition.maxTimeLimit else: @@ -347,7 +347,7 @@ def _parse_sol(self): + n_rc_lower ] - self._dual_sol = {} + self._dual_sol = dict() self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() @@ -384,24 +384,20 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition == TerminationCondition.optimal and self.config.load_solution ): for v, val in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: - results.incumbent_objective = value( + results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): + elif results.termination_condition == TerminationCondition.optimal: if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: obj_expr_evaluated = replace_expressions( self._writer.get_active_objective().expr, @@ -411,13 +407,13 @@ def _parse_sol(self): descend_into_named_expressions=True, remove_named_expressions=True, ) - results.incumbent_objective = value(obj_expr_evaluated) + results.best_feasible_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) return results @@ -435,7 +431,7 @@ def _apply_solver(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) cmd = [ @@ -481,23 +477,23 @@ def _apply_solver(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.incumbent_objective = None + results.best_feasible_objective = None else: timer.start('parse solution') results = self._parse_sol() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.objective_bound = None + results.best_objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -508,7 +504,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.incumbent_objective is None + or self._last_results_object.best_feasible_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' @@ -530,7 +526,7 @@ def get_duals( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -548,7 +544,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 4619a1c5452..b032f5c827e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,8 +1,11 @@ -from pyomo.common import unittest +from pyomo.common.errors import PyomoException +import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.contrib.solver.results import TerminationCondition +from pyomo.contrib.appsi.base import TerminationCondition +from pyomo.core.expr.numeric_expr import LinearExpression from pyomo.core.expr.taylor_series import taylor_series_expansion +from pyomo.contrib.appsi.cmodel import cmodel_available opt = Gurobi() @@ -155,12 +158,10 @@ def test_lp(self): x, y = self.get_solution() opt = Gurobi() res = opt.solve(self.m) - self.assertAlmostEqual(x + y, res.incumbent_objective) - self.assertAlmostEqual(x + y, res.objective_bound) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertTrue(res.incumbent_objective is not None) + self.assertAlmostEqual(x + y, res.best_feasible_objective) + self.assertAlmostEqual(x + y, res.best_objective_bound) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) @@ -196,11 +197,11 @@ def test_nonconvex_qcp_objective_bound_1(self): opt.gurobi_options['BestBdStop'] = -8 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.incumbent_objective, None) - self.assertAlmostEqual(res.objective_bound, -8) + self.assertEqual(res.best_feasible_objective, None) + self.assertAlmostEqual(res.best_objective_bound, -8) def test_nonconvex_qcp_objective_bound_2(self): - # the goal of this test is to ensure we can objective_bound properly + # the goal of this test is to ensure we can best_objective_bound properly # for nonconvex but continuous problems when the solver terminates with a nonzero gap # # This is a fragile test because it could fail if Gurobi's algorithms change @@ -214,8 +215,8 @@ def test_nonconvex_qcp_objective_bound_2(self): opt.gurobi_options['nonconvex'] = 2 opt.gurobi_options['MIPGap'] = 0.5 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -4) - self.assertAlmostEqual(res.objective_bound, -6) + self.assertAlmostEqual(res.best_feasible_objective, -4) + self.assertAlmostEqual(res.best_objective_bound, -6) def test_range_constraints(self): m = pe.ConcreteModel() @@ -282,7 +283,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.incumbent_objective, + res.best_feasible_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -292,7 +293,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.incumbent_objective, + res.best_feasible_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -467,7 +468,7 @@ def test_zero_time_limit(self): # what we are trying to test. Unfortunately, I'm # not sure of a good way to guarantee that if num_solutions == 0: - self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.best_feasible_objective) class TestManualModel(unittest.TestCase): diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index cd65783c566..6451db18087 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -7,6 +7,7 @@ from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output from pyomo.contrib.appsi.solvers.highs import Highs +from pyomo.contrib.appsi.base import TerminationCondition opt = Highs() @@ -32,12 +33,12 @@ def test_mutable_params_with_remove_cons(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) del m.c1 m.p2.value = 2 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -8) + self.assertAlmostEqual(res.best_feasible_objective, -8) def test_mutable_params_with_remove_vars(self): m = pe.ConcreteModel() @@ -59,14 +60,14 @@ def test_mutable_params_with_remove_vars(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) del m.c1 del m.c2 m.p1.value = -9 m.p2.value = 9 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -9) + self.assertAlmostEqual(res.best_feasible_objective, -9) def test_capture_highs_output(self): # tests issue #3003 @@ -94,7 +95,7 @@ def test_capture_highs_output(self): model[-2:-1] = [ 'opt = Highs()', - 'opt.config.tee = True', + 'opt.config.stream_solver = True', 'result = opt.solve(m)', ] with LoggingIntercept() as LOG, capture_output(capture_fd=True) as OUT: diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index 70e70fa65c5..6b86deaa535 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -from pyomo.common import unittest +import pyomo.common.unittest as unittest from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.common.gsl import find_GSL @@ -11,7 +11,7 @@ def test_external_function(self): if not DLL: self.skipTest('Could not find the amplgls.dll library') - opt = pe.SolverFactory('ipopt_v2') + opt = pe.SolverFactory('appsi_ipopt') if not opt.available(exception_flag=False): raise unittest.SkipTest @@ -31,7 +31,7 @@ def test_external_function_in_objective(self): if not DLL: self.skipTest('Could not find the amplgls.dll library') - opt = pe.SolverFactory('ipopt_v2') + opt = pe.SolverFactory('appsi_ipopt') if not opt.available(exception_flag=False): raise unittest.SkipTest diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 6731eb645fa..33f6877aaf8 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,15 +1,15 @@ import pyomo.environ as pe from pyomo.common.dependencies import attempt_import -from pyomo.common import unittest +import pyomo.common.unittest as unittest parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression +import os numpy, numpy_available = attempt_import('numpy') import random @@ -19,11 +19,17 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('highs', Highs)] -mip_solvers = [('gurobi', Gurobi), ('highs', Highs)] +all_solvers = [ + ('gurobi', Gurobi), + ('ipopt', Ipopt), + ('cplex', Cplex), + ('cbc', Cbc), + ('highs', Highs), +] +mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] -miqcqp_solvers = [('gurobi', Gurobi)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] +miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] only_child_vars_options = [True, False] @@ -62,7 +68,7 @@ def _load_tests(solver_list, only_child_vars_list): - res = [] + res = list() for solver_name, solver in solver_list: for child_var_option in only_child_vars_list: test_name = f"{solver_name}_only_child_vars_{child_var_option}" @@ -75,19 +81,17 @@ def _load_tests(solver_list, only_child_vars_list): class TestSolvers(unittest.TestCase): @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): # this test is for issue #2888 - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -95,16 +99,14 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_stale_vars( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -147,9 +149,9 @@ def test_stale_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_range_constraint( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -157,26 +159,22 @@ def test_range_constraint( m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -184,9 +182,7 @@ def test_reduced_costs( m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -195,35 +191,31 @@ def test_reduced_costs( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -244,27 +236,24 @@ def test_param_changes( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. """ - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -285,23 +274,20 @@ def test_immutable_param( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -322,23 +308,20 @@ def test_equality( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -365,19 +348,16 @@ def test_linear_expression( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_no_objective( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -389,7 +369,7 @@ def test_no_objective( m.b2 = pe.Param(mutable=True) m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) - opt.config.tee = True + opt.config.stream_solver = True params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: @@ -398,23 +378,20 @@ def test_no_objective( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertEqual(res.incumbent_objective, None) - self.assertEqual(res.objective_bound, None) + self.assertEqual(res.best_feasible_objective, None) + self.assertEqual(res.best_objective_bound, None) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -430,26 +407,22 @@ def test_add_remove_cons( m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -457,22 +430,20 @@ def test_add_remove_cons( del m.c3 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -485,25 +456,21 @@ def test_results_infeasible( res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertNotEqual(res.termination_condition, TerminationCondition.optimal) if opt_class is Ipopt: acceptable_termination_conditions = { - TerminationCondition.provenInfeasible, - TerminationCondition.locallyInfeasible, + TerminationCondition.infeasible, TerminationCondition.unbounded, } else: acceptable_termination_conditions = { - TerminationCondition.provenInfeasible, - TerminationCondition.locallyInfeasible, + TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded, } self.assertIn(res.termination_condition, acceptable_termination_conditions) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) - self.assertTrue(res.incumbent_objective is None) + self.assertTrue(res.best_feasible_objective is None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' @@ -519,10 +486,8 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_duals( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -545,9 +510,9 @@ def test_duals( @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -569,9 +534,9 @@ def test_mutable_quadratic_coefficient( @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_objective( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -596,10 +561,10 @@ def test_mutable_quadratic_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): for treat_fixed_vars_as_params in [True, False]: - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = treat_fixed_vars_as_params if not opt.available(): raise unittest.SkipTest @@ -636,9 +601,9 @@ def test_fixed_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_2( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -675,9 +640,9 @@ def test_fixed_vars_2( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_3( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -692,9 +657,9 @@ def test_fixed_vars_3( @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_fixed_vars_4( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -713,9 +678,9 @@ def test_fixed_vars_4( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_mutable_param_with_range( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest try: @@ -783,30 +748,27 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) - self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound <= m.y.value + 1e-12) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) + self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) - self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound >= m.y.value - 1e-12) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) + self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): @@ -823,9 +785,7 @@ def test_add_and_remove_vars( opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -839,9 +799,7 @@ def test_add_and_remove_vars( opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -850,9 +808,7 @@ def test_add_and_remove_vars( opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -860,9 +816,7 @@ def test_add_and_remove_vars( opt.load_vars([m.x]) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_exp( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): + def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -876,9 +830,7 @@ def test_exp( self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_log( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): + def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -893,9 +845,9 @@ def test_log( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_with_numpy( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -917,17 +869,15 @@ def test_with_numpy( ) ) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_bounds_with_params( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -959,9 +909,9 @@ def test_bounds_with_params( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1012,9 +962,9 @@ def test_solution_loader( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest from sys import platform @@ -1029,8 +979,8 @@ def test_time_limit( m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) random.seed(0) - coefs = [] - lin_vars = [] + coefs = list() + lin_vars = list() for j in m.jobs: for t in m.tasks: coefs.append(random.uniform(0, 10)) @@ -1062,13 +1012,21 @@ def test_time_limit( opt.config.time_limit = 0 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.maxTimeLimit) + if type(opt) is Cbc: # I can't figure out why CBC is reporting max iter... + self.assertIn( + res.termination_condition, + {TerminationCondition.maxIterations, TerminationCondition.maxTimeLimit}, + ) + else: + self.assertEqual( + res.termination_condition, TerminationCondition.maxTimeLimit + ) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1078,13 +1036,13 @@ def test_objective_changes( m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) m.obj = pe.Objective(expr=m.y) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.obj = pe.Objective(expr=2 * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(res.best_feasible_objective, 2) m.obj.expr = 3 * m.y res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertAlmostEqual(res.best_feasible_objective, 3) m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) @@ -1100,88 +1058,88 @@ def test_objective_changes( m.obj = pe.Objective(expr=m.x * m.y) m.x.fix(2) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 6, 6) + self.assertAlmostEqual(res.best_feasible_objective, 6, 6) m.x.fix(3) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 12, 6) + self.assertAlmostEqual(res.best_feasible_objective, 12, 6) m.x.unfix() m.y.fix(2) m.x.setlb(-3) m.x.setub(5) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -2, 6) + self.assertAlmostEqual(res.best_feasible_objective, -2, 6) m.y.unfix() m.x.setlb(None) m.x.setub(None) m.e = pe.Expression(expr=2) m.obj = pe.Objective(expr=m.e * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(res.best_feasible_objective, 2) m.e.expr = 3 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertAlmostEqual(res.best_feasible_objective, 3) opt.update_config.check_for_new_objective = False m.e.expr = 4 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 4) + self.assertAlmostEqual(res.best_feasible_objective, 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_domain( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.x.setlb(-1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.setlb(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.x.setlb(-1) m.x.domain = pe.Reals res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -1) + self.assertAlmostEqual(res.best_feasible_objective, -1) m.x.domain = pe.NonNegativeReals res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_domain_with_integers( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.x.setlb(-5.5) m.x.domain = pe.Integers res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -5) + self.assertAlmostEqual(res.best_feasible_objective, -5) m.x.domain = pe.Binary res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_binaries( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1191,25 +1149,25 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1227,15 +1185,20 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt.use_extensions = True + res = opt.solve(m) + self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere( - self, name: str, opt_class: Type[PersistentSolverBase] - ): - opt: PersistentSolverBase = opt_class(only_child_vars=False) + def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): + opt: PersistentSolver = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1248,27 +1211,21 @@ def test_variables_elsewhere( m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2( - self, name: str, opt_class: Type[PersistentSolverBase] - ): - opt: PersistentSolverBase = opt_class(only_child_vars=False) + def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver]): + opt: PersistentSolver = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1284,10 +1241,8 @@ def test_variables_elsewhere2( m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) @@ -1296,20 +1251,16 @@ def test_variables_elsewhere2( del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_1( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1322,28 +1273,22 @@ def test_bug_1( m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 3) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_2( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): + def test_bug_2(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. """ for fixed_var_option in [True, False]: - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest opt.update_config.treat_fixed_vars_as_params = fixed_var_option @@ -1356,19 +1301,19 @@ def test_bug_2( m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 2, 5) + self.assertAlmostEqual(res.best_feasible_objective, 2, 5) m.x.unfix() m.x.setlb(-9) m.x.setub(9) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -18, 5) + self.assertAlmostEqual(res.best_feasible_objective, -18, 5) @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestLegacySolverInterface(unittest.TestCase): @parameterized.expand(input=all_solvers) - def test_param_updates(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest @@ -1398,7 +1343,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolverBase]): self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) - def test_load_solutions(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index e09865294eb..d250923f104 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus +from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math @@ -18,18 +18,12 @@ def test_param_updates(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) m.p.value = 2 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) def test_remove_add_constraint(self): @@ -42,10 +36,7 @@ def test_remove_add_constraint(self): opt.config.symbolic_solver_labels = True opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -54,10 +45,7 @@ def test_remove_add_constraint(self): m.x.value = 0.5 m.y.value = 0.5 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 0) @@ -70,30 +58,21 @@ def test_fixed_var(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) m.x.unfix() m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) m.x.fix(0.5) del m.c2 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -110,10 +89,7 @@ def test_remove_variables_params(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) self.assertAlmostEqual(m.z.value, 0) @@ -124,20 +100,14 @@ def test_remove_variables_params(self): m.z.value = 2 m.px.value = 2 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) self.assertAlmostEqual(m.z.value, 2) del m.z m.px.value = 3 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 3) def test_get_primals(self): @@ -150,10 +120,7 @@ def test_get_primals(self): opt.config.load_solution = False opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) primals = opt.get_primals() @@ -167,73 +134,49 @@ def test_operators(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) del m.c1 m.x.value = 0 m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, math.pi / 2) del m.c1 m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) m.x.value = 0 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.6) del m.c1 m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.6) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 04f54530c1b..0a358c6aedf 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,8 +1,11 @@ -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.util import PersistentSolverUtils -from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus -from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.contrib.appsi.base import ( + PersistentBase, + PersistentSolver, + SolverConfig, + Results, + TerminationCondition, + PersistentSolutionLoader, +) from pyomo.core.expr.numeric_expr import ( ProductExpression, DivisionExpression, @@ -33,6 +36,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available wntr, wntr_available = attempt_import('wntr') import logging @@ -65,10 +69,11 @@ def __init__( class WntrResults(Results): def __init__(self, solver): super().__init__() + self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) -class Wntr(PersistentSolverUtils, PersistentSolverBase): +class Wntr(PersistentBase, PersistentSolver): def __init__(self, only_child_vars=True): super().__init__(only_child_vars=only_child_vars) self._config = WntrConfig() @@ -121,7 +126,7 @@ def _solve(self, timer: HierarchicalTimer): options.update(self.wntr_options) opt = wntr.sim.solvers.NewtonSolver(options) - if self.config.tee: + if self.config.stream_solver: ostream = sys.stdout else: ostream = None @@ -138,14 +143,13 @@ def _solve(self, timer: HierarchicalTimer): tf = time.time() results = WntrResults(self) - results.timing_info.wall_time = tf - t0 + results.wallclock_time = tf - t0 if status == wntr.sim.solvers.SolverStatus.converged: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) - results.solution_status = SolutionStatus.optimal + results.termination_condition = TerminationCondition.optimal else: results.termination_condition = TerminationCondition.error + results.best_feasible_objective = None + results.best_objective_bound = None if self.config.load_solution: if status == wntr.sim.solvers.SolverStatus.converged: @@ -157,7 +161,7 @@ def _solve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) return results @@ -208,6 +212,8 @@ def set_instance(self, model): ) self._reinit() self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py new file mode 100644 index 00000000000..0d67ca4d01a --- /dev/null +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -0,0 +1,91 @@ +from pyomo.common import unittest +from pyomo.contrib import appsi +import pyomo.environ as pe +from pyomo.core.base.var import ScalarVar + + +class TestResults(unittest.TestCase): + def test_uninitialized(self): + res = appsi.base.Results() + self.assertIsNone(res.best_feasible_objective) + self.assertIsNone(res.best_objective_bound) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.unknown + ) + + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid slacks.*' + ): + res.solution_loader.get_slacks() + + def test_results(self): + m = pe.ConcreteModel() + m.x = ScalarVar() + m.y = ScalarVar() + m.c1 = pe.Constraint(expr=m.x == 1) + m.c2 = pe.Constraint(expr=m.y == 2) + + primals = dict() + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = dict() + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = dict() + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + slacks = dict() + slacks[m.c1] = 7 + slacks[m.c2] = 8 + + res = appsi.base.Results() + res.solution_loader = appsi.base.SolutionLoader( + primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + slacks2 = res.solution_loader.get_slacks() + self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) + + slacks2 = res.solution_loader.get_slacks([m.c2]) + self.assertNotIn(m.c1, slacks2) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 0924e3bbeed..7963cc31665 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,5 +1,5 @@ from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available -from pyomo.common import unittest +import pyomo.common.unittest as unittest import math from pyomo.contrib.fbbt.tests.test_interval import IntervalTestBase @@ -7,7 +7,7 @@ @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestInterval(IntervalTestBase, unittest.TestCase): def setUp(self): - super().setUp() + super(TestInterval, self).setUp() self.add = cmodel.py_interval_add self.sub = cmodel.py_interval_sub self.mul = cmodel.py_interval_mul diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py new file mode 100644 index 00000000000..f665736fd4a --- /dev/null +++ b/pyomo/contrib/appsi/utils/__init__.py @@ -0,0 +1,2 @@ +from .get_objective import get_objective +from .collect_vars_and_named_exprs import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py new file mode 100644 index 00000000000..9027080f08c --- /dev/null +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -0,0 +1,50 @@ +from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types +import pyomo.core.expr as EXPR + + +class _VarAndNamedExprCollector(ExpressionValueVisitor): + def __init__(self): + self.named_expressions = dict() + self.variables = dict() + self.fixed_vars = dict() + self._external_functions = dict() + + def visit(self, node, values): + pass + + def visiting_potential_leaf(self, node): + if type(node) in nonpyomo_leaf_types: + return True, None + + if node.is_variable_type(): + self.variables[id(node)] = node + if node.is_fixed(): + self.fixed_vars[id(node)] = node + return True, None + + if node.is_named_expression_type(): + self.named_expressions[id(node)] = node + return False, None + + if type(node) is EXPR.ExternalFunctionExpression: + self._external_functions[id(node)] = node + return False, None + + if node.is_expression_type(): + return False, None + + return True, None + + +_visitor = _VarAndNamedExprCollector() + + +def collect_vars_and_named_exprs(expr): + _visitor.__init__() + _visitor.dfs_postorder_stack(expr) + return ( + list(_visitor.named_expressions.values()), + list(_visitor.variables.values()), + list(_visitor.fixed_vars.values()), + list(_visitor._external_functions.values()), + ) diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py new file mode 100644 index 00000000000..30dd911f9c8 --- /dev/null +++ b/pyomo/contrib/appsi/utils/get_objective.py @@ -0,0 +1,12 @@ +from pyomo.core.base.objective import Objective + + +def get_objective(block): + obj = None + for o in block.component_data_objects( + Objective, descend_into=True, active=True, sort=True + ): + if obj is not None: + raise ValueError('Multiple active objectives found') + obj = o + return obj diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py new file mode 100644 index 00000000000..4c2a167a017 --- /dev/null +++ b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py @@ -0,0 +1,56 @@ +from pyomo.common import unittest +import pyomo.environ as pe +from pyomo.contrib.appsi.utils import collect_vars_and_named_exprs +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available +from typing import Callable +from pyomo.common.gsl import find_GSL + + +class TestCollectVarsAndNamedExpressions(unittest.TestCase): + def basics_helper(self, collector: Callable, *args): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.E = pe.Expression(expr=2 * m.z + 1) + m.y.fix(3) + e = m.x * m.y + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.x, m.y, m.z], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([], external_funcs) + + def test_basics(self): + self.basics_helper(collect_vars_and_named_exprs) + + @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') + def test_basics_cmodel(self): + self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) + + def external_func_helper(self, collector: Callable, *args): + DLL = find_GSL() + if not DLL: + self.skipTest('Could not find amplgsl.dll library') + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.hypot = pe.ExternalFunction(library=DLL, function='gsl_hypot') + func = m.hypot(m.x, m.x * m.y) + m.E = pe.Expression(expr=2 * func) + m.y.fix(3) + e = m.z + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.z, m.x, m.y], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([func], external_funcs) + + def test_external(self): + self.external_func_helper(collect_vars_and_named_exprs) + + @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') + def test_external_cmodel(self): + self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 2a4e638f097..7a7faadaabe 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,3 @@ -class WriterConfig: +class WriterConfig(object): def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 9d0b71fe794..8a76fa5f9eb 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -5,17 +5,19 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData +from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.core.expr.numvalue import value +from pyomo.contrib.appsi.base import PersistentBase from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer -from pyomo.core.kernel.objective import minimize -from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.core.kernel.objective import minimize, maximize from .config import WriterConfig from ..cmodel import cmodel, cmodel_available -class LPWriter(PersistentSolverUtils): +class LPWriter(PersistentBase): def __init__(self, only_child_vars=False): - super().__init__(only_child_vars=only_child_vars) + super(LPWriter, self).__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() @@ -23,11 +25,11 @@ def __init__(self, only_child_vars=False): self._con_labeler = None self._param_labeler = None self._obj_labeler = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_var_to_pyomo_var_map = {} - self._solver_con_to_pyomo_con_map = {} - self._pyomo_param_to_solver_param_map = {} + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_var_to_pyomo_var_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_param_to_solver_param_map = dict() self._expr_types = None @property @@ -87,7 +89,7 @@ def _add_params(self, params: List[_ParamData]): self._pyomo_param_to_solver_param_map[id(p)] = cp def _add_constraints(self, cons: List[_GeneralConstraintData]): - cmodel.process_lp_constraints() + cmodel.process_lp_constraints(cons, self) def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index a9b44e63f36..9c739fd6ebb 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,6 +1,4 @@ -import os from typing import List - from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData @@ -8,31 +6,32 @@ from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.core.base import SymbolMap, TextLabeler +from pyomo.core.expr.numvalue import value +from pyomo.contrib.appsi.base import PersistentBase +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from pyomo.common.collections import OrderedSet -from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.contrib.solver.util import PersistentSolverUtils - from .config import WriterConfig +from pyomo.common.collections import OrderedSet +import os from ..cmodel import cmodel, cmodel_available +from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -class NLWriter(PersistentSolverUtils): +class NLWriter(PersistentBase): def __init__(self, only_child_vars=False): - super().__init__(only_child_vars=only_child_vars) + super(NLWriter, self).__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() self._var_labeler = None self._con_labeler = None self._param_labeler = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_var_to_pyomo_var_map = {} - self._solver_con_to_pyomo_con_map = {} - self._pyomo_param_to_solver_param_map = {} + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_var_to_pyomo_var_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_param_to_solver_param_map = dict() self._expr_types = None @property @@ -173,8 +172,8 @@ def update_params(self): def _set_objective(self, obj: _GeneralObjectiveData): if obj is None: const = cmodel.Constant(0) - lin_vars = [] - lin_coef = [] + lin_vars = list() + lin_coef = list() nonlin = cmodel.Constant(0) sense = 0 else: @@ -241,7 +240,7 @@ def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = Non timer.stop('write file') def update(self, timer: HierarchicalTimer = None): - super().update(timer=timer) + super(NLWriter, self).update(timer=timer) self._set_pyomo_amplfunc_env() def get_ordered_vars(self): diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index 297bc3d7617..3b61a5901c3 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,4 +1,4 @@ -from pyomo.common import unittest +import pyomo.common.unittest as unittest from pyomo.common.tempfiles import TempfileManager import pyomo.environ as pe from pyomo.contrib import appsi From c0d35302b00a5b41752fd641a2f9c098fb5b8b3e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Jan 2024 10:10:48 -0700 Subject: [PATCH 0224/1178] Explicitly register contrib.solver --- pyomo/environ/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..5d488bda290 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -50,6 +50,7 @@ def _do_import(pkg_name): 'pyomo.contrib.multistart', 'pyomo.contrib.preprocessing', 'pyomo.contrib.pynumero', + 'pyomo.contrib.solver', 'pyomo.contrib.trustregion', ] From a1be778f394f2a92200e7ec5fb1ef8ea9c7b4ba1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Jan 2024 10:12:49 -0700 Subject: [PATCH 0225/1178] Fix linking in online docs --- doc/OnlineDocs/developer_reference/solvers.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 75d95fc36db..10e7e829463 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -3,7 +3,7 @@ Solver Interfaces Pyomo offers interfaces into multiple solvers, both commercial and open source. -.. currentmodule:: pyomo.solver +.. currentmodule:: pyomo.contrib.solver Interface Implementation @@ -19,7 +19,7 @@ Every solver, at the end of a ``solve`` call, will return a ``Results`` object. This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar to a standard ``dict`` in Python. -.. autoclass:: pyomo.solver.results.Results +.. autoclass:: pyomo.contrib.solver.results.Results :show-inheritance: :members: :undoc-members: @@ -35,7 +35,7 @@ returned solver messages or logs for more information. -.. autoclass:: pyomo.solver.results.TerminationCondition +.. autoclass:: pyomo.contrib.solver.results.TerminationCondition :show-inheritance: :noindex: @@ -48,7 +48,7 @@ intent of ``SolutionStatus`` is to notify the user of what the solver returned at a high level. The user is expected to inspect the ``Results`` object or any returned solver messages or logs for more information. -.. autoclass:: pyomo.solver.results.SolutionStatus +.. autoclass:: pyomo.contrib.solver.results.SolutionStatus :show-inheritance: :noindex: From c77eb247b0910b45c986d27ee9194a3b5926193f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 14:18:20 -0700 Subject: [PATCH 0226/1178] rework ipopt solution loader --- pyomo/contrib/solver/ipopt.py | 64 +++++++++++++++++++----------- pyomo/contrib/solver/sol_reader.py | 50 +++++------------------ pyomo/contrib/solver/solution.py | 44 +++++++++++++++++++- 3 files changed, 92 insertions(+), 66 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 63dca0af0d9..4e2dcdf83ab 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -14,27 +14,28 @@ import datetime import io import sys -from typing import Mapping, Optional, Dict +from typing import Mapping, Optional, Sequence from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer -from pyomo.core.base import Objective +from pyomo.core.base.var import _GeneralVarData from pyomo.core.staleflag import StaleFlagManager -from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from .sol_reader import parse_sol_file -from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader +from pyomo.contrib.solver.solution import SolSolutionLoader, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix +from pyomo.common.collections import ComponentMap import logging @@ -110,8 +111,38 @@ def __init__( ) -class ipoptSolutionLoader(SolutionLoaderBase): - pass +class ipoptSolutionLoader(SolSolutionLoader): + def get_reduced_costs(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + sol_data = self._sol_data + nl_info = self._nl_info + zl_map = sol_data.var_suffixes['ipopt_zL_out'] + zu_map = sol_data.var_suffixes['ipopt_zU_out'] + rc = dict() + for v in nl_info.variables: + v_id = id(v) + rc[v_id] = (v, 0) + if v_id in zl_map: + zl = zl_map[v_id][1] + if abs(zl) > abs(rc[v_id][1]): + rc[v_id] = (v, zl) + if v_id in zu_map: + zu = zu_map[v_id][1] + if abs(zu) > abs(rc[v_id][1]): + rc[v_id] = (v, zu) + + if vars_to_load is None: + res = ComponentMap(rc.values()) + for v, _ in nl_info.eliminated_vars: + res[v] = 0 + else: + res = ComponentMap() + for v in vars_to_load: + if id(v) in rc: + res[v] = rc[id(v)][1] + else: + # eliminated vars + res[v] = 0 + return res ipopt_command_line_options = { @@ -452,24 +483,9 @@ def _parse_solution( if res.solution_status == SolutionStatus.noSolution: res.solution_loader = SolutionLoader(None, None, None, None) else: - rc = dict() - for v in nl_info.variables: - v_id = id(v) - rc[v_id] = (v, 0) - if v_id in sol_data.var_suffixes['ipopt_zL_out']: - zl = sol_data.var_suffixes['ipopt_zL_out'][v_id][1] - if abs(zl) > abs(rc[v_id][1]): - rc[v_id] = (v, zl) - if v_id in sol_data.var_suffixes['ipopt_zU_out']: - zu = sol_data.var_suffixes['ipopt_zU_out'][v_id][1] - if abs(zu) > abs(rc[v_id][1]): - rc[v_id] = (v, zu) - - res.solution_loader = SolutionLoader( - primals=sol_data.primals, - duals=sol_data.duals, - slacks=None, - reduced_costs=rc, + res.solution_loader = ipoptSolutionLoader( + sol_data=sol_data, + nl_info=nl_info, ) return res diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 92761246241..28fe0100015 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -9,24 +9,13 @@ from pyomo.repn.plugins.nl_writer import AMPLRepn -def evaluate_ampl_repn(repn: AMPLRepn, sub_map): - assert not repn.nonlinear - assert repn.nl is None - val = repn.const - if repn.linear is not None: - for v_id, v_coef in repn.linear.items(): - val += v_coef * sub_map[v_id] - val *= repn.mult - return val - - class SolFileData: def __init__(self) -> None: - self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() - self.duals: Dict[_ConstraintData, float] = dict() - self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() - self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() - self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() + self.primals: List[float] = list() + self.duals: List[float] = list() + self.var_suffixes: Dict[str, Dict[int, Any]] = dict() + self.con_suffixes: Dict[str, Dict[Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Any]] = dict() self.problem_suffixes: Dict[str, List[Any]] = dict() @@ -138,10 +127,8 @@ def parse_sol_file( result.extra_info.solver_message = exit_code_message if result.solution_status != SolutionStatus.noSolution: - for v, val in zip(nl_info.variables, variable_vals): - sol_data.primals[id(v)] = (v, val) - for c, val in zip(nl_info.constraints, duals): - sol_data.duals[c] = val + sol_data.primals = variable_vals + sol_data.duals = duals ### Read suffixes ### line = sol_file.readline() while line: @@ -180,18 +167,13 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() var_ndx = int(suf_line[0]) - var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = ( - var, - convert_function(suf_line[1]), - ) + sol_data.var_suffixes[suffix_name][var_ndx] = convert_function(suf_line[1]) elif kind == 1: # Con sol_data.con_suffixes[suffix_name] = dict() for cnt in range(nvalues): suf_line = sol_file.readline().split() con_ndx = int(suf_line[0]) - con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function( + sol_data.con_suffixes[suffix_name][con_ndx] = convert_function( suf_line[1] ) elif kind == 2: # Obj @@ -199,11 +181,7 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() obj_ndx = int(suf_line[0]) - obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = ( - obj, - convert_function(suf_line[1]), - ) + sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function(suf_line[1]) elif kind == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() for cnt in range(nvalues): @@ -213,12 +191,4 @@ def parse_sol_file( ) line = sol_file.readline() - if len(nl_info.eliminated_vars) > 0: - sub_map = {k: v[1] for k, v in sol_data.primals.items()} - for v, v_expr in nl_info.eliminated_vars: - val = evaluate_ampl_repn(v_expr, sub_map) - v_id = id(v) - sub_map[v_id] = val - sol_data.primals[v_id] = (v, val) - return result, sol_data diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 4ec3f98cd08..7ea26c5f484 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -16,6 +16,10 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager +from .sol_reader import SolFileData +from pyomo.repn.plugins.nl_writer import NLWriterInfo, AMPLRepn +from pyomo.core.expr.numvalue import value +from pyomo.core.expr.visitor import replace_expressions # CHANGES: # - `load` method: should just load the whole thing back into the model; load_solution = True @@ -49,8 +53,9 @@ def load_vars( Parameters ---------- vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. + The minimum set of variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. Even if vars_to_load is specified, the values of other + variables may also be loaded depending on the interface. """ for v, val in self.get_primals(vars_to_load=vars_to_load).items(): v.set_value(val, skip_validation=True) @@ -195,6 +200,41 @@ def get_reduced_costs( return rc +class SolSolutionLoader(SolutionLoaderBase): + def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: + self._sol_data = sol_data + self._nl_info = nl_info + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) + + for v, v_expr in self._nl_info.eliminated_vars: + v.set_value(value(v_expr), skip_validation=True) + + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + val_map = dict(zip([id(v) for v in self._nl_info.variables], self._sol_data.primals)) + + for v, v_expr in self._nl_info.eliminated_vars: + val = replace_expressions(v_expr, substitution_map=val_map) + v_id = id(v) + val_map[v_id] = val + + res = ComponentMap() + for v in vars_to_load: + res[v] = val_map[id(v)] + + return res + + def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None) -> Dict[_GeneralConstraintData, float]: + cons_to_load = set(cons_to_load) + return {c: val for c, val in zip(self._nl_info.constraints, self._sol_data.duals) if c in cons_to_load} + + class PersistentSolutionLoader(SolutionLoaderBase): def __init__(self, solver): self._solver = solver From a5e3873e6c37b54e115baed35a2999f6cbd99f98 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 14:32:36 -0700 Subject: [PATCH 0227/1178] fix imports --- pyomo/contrib/solver/results.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index e21adcc35cc..b4a30da0b35 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -28,7 +28,6 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) -from pyomo.contrib.solver.solution import SolutionLoaderBase class SolverResultsError(PyomoException): @@ -192,7 +191,7 @@ def __init__( visibility=visibility, ) - self.solution_loader: SolutionLoaderBase = self.declare( + self.solution_loader = self.declare( 'solution_loader', ConfigValue() ) self.termination_condition: TerminationCondition = self.declare( From f1bd6821001233423d7bb7ac123f5560c6340c7d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 14:45:41 -0700 Subject: [PATCH 0228/1178] override display() --- pyomo/contrib/solver/results.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index b4a30da0b35..fa543d0aeaa 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -244,6 +244,9 @@ def __init__( ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), ) + def display(self, content_filter=None, indent_spacing=2, ostream=None, visibility=0): + return super().display(content_filter, indent_spacing, ostream, visibility) + class ResultsReader: pass From a44929ce6ff58adf9a638895261ba25b8a21e2b4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 15:19:45 -0700 Subject: [PATCH 0229/1178] better handling of solver output --- pyomo/contrib/solver/config.py | 8 ++++++++ pyomo/contrib/solver/ipopt.py | 11 +++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 84b1c2d2c87..d053356a684 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -50,6 +50,14 @@ def __init__( description="If True, the solver log prints to stdout.", ), ) + self.log_solver_output: bool = self.declare( + 'log_solver_output', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver output gets logged.", + ), + ) self.load_solution: bool = self.declare( 'load_solution', ConfigValue( diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 4e2dcdf83ab..1b091638343 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -69,15 +69,10 @@ def __init__( 'executable', ConfigValue(default=Executable('ipopt')) ) # TODO: Add in a deprecation here for keepfiles + # M.B.: Is the above TODO still relevant? self.temp_dir: str = self.declare( 'temp_dir', ConfigValue(domain=str, default=None) ) - self.solver_output_logger = self.declare( - 'solver_output_logger', ConfigValue(default=logger) - ) - self.log_level = self.declare( - 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) - ) self.writer_config = self.declare( 'writer_config', ConfigValue(default=NLWriter.CONFIG()) ) @@ -347,10 +342,10 @@ def solve(self, model, **kwds): ostreams = [io.StringIO()] if config.tee: ostreams.append(sys.stdout) - else: + if config.log_solver_output: ostreams.append( LogStream( - level=config.log_level, logger=config.solver_output_logger + level=logging.INFO, logger=logger ) ) with TeeStream(*ostreams) as t: From a3fe00381b83c7cd98797da62c967651594efb90 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 16:35:20 -0700 Subject: [PATCH 0230/1178] handle scaling when loading results --- pyomo/contrib/solver/ipopt.py | 15 ++++++++++----- pyomo/contrib/solver/solution.py | 33 +++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 1b091638343..47436d9d11f 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -108,20 +108,25 @@ def __init__( class ipoptSolutionLoader(SolSolutionLoader): def get_reduced_costs(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables sol_data = self._sol_data nl_info = self._nl_info zl_map = sol_data.var_suffixes['ipopt_zL_out'] zu_map = sol_data.var_suffixes['ipopt_zU_out'] rc = dict() - for v in nl_info.variables: + for ndx, v in enumerate(nl_info.variables): + scale = scale_list[ndx] v_id = id(v) rc[v_id] = (v, 0) - if v_id in zl_map: - zl = zl_map[v_id][1] + if ndx in zl_map: + zl = zl_map[ndx] * scale if abs(zl) > abs(rc[v_id][1]): rc[v_id] = (v, zl) - if v_id in zu_map: - zu = zu_map[v_id][1] + if ndx in zu_map: + zu = zu_map[ndx] * scale if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 7ea26c5f484..ae47491c310 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -208,8 +208,12 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: - for v, val in zip(self._nl_info.variables, self._sol_data.primals): - v.set_value(val, skip_validation=True) + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): + v.set_value(val/scale, skip_validation=True) for v, v_expr in self._nl_info.eliminated_vars: v.set_value(value(v_expr), skip_validation=True) @@ -217,7 +221,13 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: - val_map = dict(zip([id(v) for v in self._nl_info.variables], self._sol_data.primals)) + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + val_map = dict() + for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): + val_map[id(v)] = val / scale for v, v_expr in self._nl_info.eliminated_vars: val = replace_expressions(v_expr, substitution_map=val_map) @@ -225,14 +235,27 @@ def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> val_map[v_id] = val res = ComponentMap() + if vars_to_load is None: + vars_to_load = self._nl_info.variables + [v for v, _ in self._nl_info.eliminated_vars] for v in vars_to_load: res[v] = val_map[id(v)] return res def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None) -> Dict[_GeneralConstraintData, float]: - cons_to_load = set(cons_to_load) - return {c: val for c, val in zip(self._nl_info.constraints, self._sol_data.duals) if c in cons_to_load} + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.constraints) + else: + scale_list = self._nl_info.scaling.constraints + if cons_to_load is None: + cons_to_load = set(self._nl_info.constraints) + else: + cons_to_load = set(cons_to_load) + res = dict() + for c, val, scale in zip(self._nl_info.constraints, self._sol_data.duals, scale_list): + if c in cons_to_load: + res[c] = val * scale + return res class PersistentSolutionLoader(SolutionLoaderBase): From da2e58f48c2043b01844fb2909793d2512107624 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 07:53:42 -0700 Subject: [PATCH 0231/1178] Save state: fixing broken tests --- pyomo/contrib/solver/config.py | 3 +++ pyomo/contrib/solver/tests/unit/test_base.py | 16 +++++----------- pyomo/contrib/solver/tests/unit/test_config.py | 17 ++++++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 84b1c2d2c87..f5aa1e2c5c7 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -141,10 +141,13 @@ class AutoUpdateConfig(ConfigDict): check_for_new_or_removed_constraints: bool check_for_new_or_removed_vars: bool check_for_new_or_removed_params: bool + check_for_new_objective: bool update_constraints: bool update_vars: bool update_params: bool update_named_expressions: bool + update_objective: bool + treat_fixed_vars_as_params: bool """ def __init__( diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 71690b7aa0e..e3a8999d8c5 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -19,7 +19,7 @@ def test_solver_base(self): self.instance = base.SolverBase() self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) - self.assertEqual(self.instance.config, None) + self.assertEqual(self.instance.CONFIG, self.instance.config) self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) @@ -39,18 +39,16 @@ def test_abstract_member_list(self): expected_list = [ 'remove_params', 'version', - 'config', 'update_variables', 'remove_variables', 'add_constraints', - 'get_primals', + '_get_primals', 'set_instance', 'set_objective', 'update_params', 'remove_block', 'add_block', 'available', - 'update_config', 'add_params', 'remove_constraints', 'add_variables', @@ -63,7 +61,6 @@ def test_abstract_member_list(self): def test_persistent_solver_base(self): self.instance = base.PersistentSolverBase() self.assertTrue(self.instance.is_persistent()) - self.assertEqual(self.instance.update_config, None) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) self.assertEqual(self.instance.add_params(None), None) @@ -78,13 +75,10 @@ def test_persistent_solver_base(self): self.assertEqual(self.instance.update_params(), None) with self.assertRaises(NotImplementedError): - self.instance.get_primals() + self.instance._get_primals() with self.assertRaises(NotImplementedError): - self.instance.get_duals() + self.instance._get_duals() with self.assertRaises(NotImplementedError): - self.instance.get_slacks() - - with self.assertRaises(NotImplementedError): - self.instance.get_reduced_costs() + self.instance._get_reduced_costs() diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index 1051825f4e5..3ad8319343b 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -16,12 +16,15 @@ class TestSolverConfig(unittest.TestCase): def test_interface_default_instantiation(self): config = SolverConfig() - self.assertEqual(config._description, None) + self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) self.assertTrue(config.load_solution) + self.assertTrue(config.raise_exception_on_nonoptimal_result) self.assertFalse(config.symbolic_solver_labels) - self.assertFalse(config.report_timing) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) def test_interface_custom_instantiation(self): config = SolverConfig(description="A description") @@ -31,20 +34,19 @@ def test_interface_custom_instantiation(self): self.assertFalse(config.time_limit) config.time_limit = 1.0 self.assertEqual(config.time_limit, 1.0) + self.assertIsInstance(config.time_limit, float) class TestBranchAndBoundConfig(unittest.TestCase): def test_interface_default_instantiation(self): config = BranchAndBoundConfig() - self.assertEqual(config._description, None) + self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) self.assertTrue(config.load_solution) self.assertFalse(config.symbolic_solver_labels) - self.assertFalse(config.report_timing) - self.assertEqual(config.rel_gap, None) - self.assertEqual(config.abs_gap, None) - self.assertFalse(config.relax_integrality) + self.assertIsNone(config.rel_gap) + self.assertIsNone(config.abs_gap) def test_interface_custom_instantiation(self): config = BranchAndBoundConfig(description="A description") @@ -54,5 +56,6 @@ def test_interface_custom_instantiation(self): self.assertFalse(config.time_limit) config.time_limit = 1.0 self.assertEqual(config.time_limit, 1.0) + self.assertIsInstance(config.time_limit, float) config.rel_gap = 2.5 self.assertEqual(config.rel_gap, 2.5) From 7fa02de0cc8b87afa6e13b392eb2651af88ee9bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:11:03 -0700 Subject: [PATCH 0232/1178] Remove slack referrences; fix broken unit tests --- pyomo/contrib/solver/base.py | 8 ------ pyomo/contrib/solver/ipopt.py | 4 +-- pyomo/contrib/solver/solution.py | 27 +------------------ .../solver/tests/solvers/test_ipopt.py | 5 ++-- .../contrib/solver/tests/unit/test_results.py | 20 +++----------- .../solver/tests/unit/test_solution.py | 2 -- 6 files changed, 9 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 962d35582a1..0b33f8a5648 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -435,9 +435,6 @@ def solve( if hasattr(model, 'dual') and model.dual.import_enabled(): for c, val in results.solution_loader.get_duals().items(): model.dual[c] = val - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - model.slack[c] = val if hasattr(model, 'rc') and model.rc.import_enabled(): for v, val in results.solution_loader.get_reduced_costs().items(): model.rc[v] = val @@ -448,11 +445,6 @@ def solve( if hasattr(model, 'dual') and model.dual.import_enabled(): for c, val in results.solution_loader.get_duals().items(): legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - symbol = symbol_map.getSymbol(c) - if symbol in legacy_soln.constraint: - legacy_soln.constraint[symbol]['Slack'] = val if hasattr(model, 'rc') and model.rc.import_enabled(): for v, val in results.solution_loader.get_reduced_costs().items(): legacy_soln.variable['Rc'] = val diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 47436d9d11f..6ab40fd3924 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -372,7 +372,7 @@ def solve(self, model, **kwds): if process.returncode != 0: results.termination_condition = TerminationCondition.error - results.solution_loader = SolutionLoader(None, None, None, None) + results.solution_loader = SolutionLoader(None, None, None) else: with open(basename + '.sol', 'r') as sol_file: timer.start('parse_sol') @@ -481,7 +481,7 @@ def _parse_solution( ) if res.solution_status == SolutionStatus.noSolution: - res.solution_loader = SolutionLoader(None, None, None, None) + res.solution_loader = SolutionLoader(None, None, None) else: res.solution_loader = ipoptSolutionLoader( sol_data=sol_data, diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index ae47491c310..18fd96759cf 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -17,31 +17,10 @@ from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager from .sol_reader import SolFileData -from pyomo.repn.plugins.nl_writer import NLWriterInfo, AMPLRepn +from pyomo.repn.plugins.nl_writer import NLWriterInfo from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions -# CHANGES: -# - `load` method: should just load the whole thing back into the model; load_solution = True -# - `load_variables` -# - `get_variables` -# - `get_constraints` -# - `get_objective` -# - `get_slacks` -# - `get_reduced_costs` - -# duals is how much better you could get if you weren't constrained. -# dual value of 0 means that the constraint isn't actively constraining anything. -# high dual value means that it is costing us a lot in the objective. -# can also be called "shadow price" - -# bounds on variables are implied constraints. -# getting a dual on the bound of a variable is the reduced cost. -# IPOPT calls these the bound multipliers (normally they are reduced costs, though). ZL, ZU - -# slacks are... something that I don't understand -# but they are necessary somewhere? I guess? - class SolutionLoaderBase(abc.ABC): def load_vars( @@ -129,7 +108,6 @@ def __init__( self, primals: Optional[MutableMapping], duals: Optional[MutableMapping], - slacks: Optional[MutableMapping], reduced_costs: Optional[MutableMapping], ): """ @@ -139,14 +117,11 @@ def __init__( maps id(Var) to (var, value) duals: dict maps Constraint to dual value - slacks: dict - maps Constraint to slack value reduced_costs: dict maps id(Var) to (var, reduced_cost) """ self._primals = primals self._duals = duals - self._slacks = slacks self._reduced_costs = reduced_costs def get_primals( diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index c1aecba05fc..9638d94bdda 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -45,13 +45,12 @@ def test_ipopt_config(self): config = ipoptConfig() self.assertTrue(config.load_solution) self.assertIsInstance(config.solver_options, ConfigDict) - print(type(config.executable)) self.assertIsInstance(config.executable, ExecutableData) # Test custom initialization - solver = SolverFactory('ipopt_v2', save_solver_io=True) - self.assertTrue(solver.config.save_solver_io) + solver = SolverFactory('ipopt_v2', executable='/path/to/exe') self.assertFalse(solver.config.tee) + self.assertTrue(solver.config.executable.startswith('/path')) # Change value on a solve call # model = self.create_model() diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index e7d02751f7d..927ab64ee12 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -82,6 +82,8 @@ def test_declared_items(self): 'solver_version', 'termination_condition', 'timing_info', + 'solver_log', + 'solver_configuration' } actual_declared = res._declared self.assertEqual(expected_declared, actual_declared) @@ -101,7 +103,7 @@ def test_uninitialized(self): self.assertIsInstance(res.extra_info, ConfigDict) self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) - res.solution_loader = solution.SolutionLoader(None, None, None, None) + res.solution_loader = solution.SolutionLoader(None, None, None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' @@ -115,10 +117,6 @@ def test_uninitialized(self): RuntimeError, '.*does not currently have valid reduced costs.*' ): res.solution_loader.get_reduced_costs() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid slacks.*' - ): - res.solution_loader.get_slacks() def test_results(self): m = pyo.ConcreteModel() @@ -136,13 +134,10 @@ def test_results(self): rc = {} rc[id(m.x)] = (m.x, 5) rc[id(m.y)] = (m.y, 6) - slacks = {} - slacks[m.c1] = 7 - slacks[m.c2] = 8 res = results.Results() res.solution_loader = solution.SolutionLoader( - primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + primals=primals, duals=duals, reduced_costs=rc ) res.solution_loader.load_vars() @@ -172,10 +167,3 @@ def test_results(self): self.assertNotIn(m.x, rc2) self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - slacks2 = res.solution_loader.get_slacks() - self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) - - slacks2 = res.solution_loader.get_slacks([m.c2]) - self.assertNotIn(m.c1, slacks2) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index dc53f1e4543..1ecba45b32a 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -27,7 +27,5 @@ def test_solution_loader_base(self): self.assertEqual(self.instance.get_primals(), None) with self.assertRaises(NotImplementedError): self.instance.get_duals() - with self.assertRaises(NotImplementedError): - self.instance.get_slacks() with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() From efb1eee6d471c88a18670ed91c7baea0edbdd491 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:11:51 -0700 Subject: [PATCH 0233/1178] Apply black --- pyomo/contrib/solver/ipopt.py | 13 ++++---- pyomo/contrib/solver/results.py | 8 ++--- pyomo/contrib/solver/sol_reader.py | 8 +++-- pyomo/contrib/solver/solution.py | 30 +++++++++++++------ .../contrib/solver/tests/unit/test_results.py | 3 +- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 6ab40fd3924..516c0fd7f4b 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -107,7 +107,9 @@ def __init__( class ipoptSolutionLoader(SolSolutionLoader): - def get_reduced_costs(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + def get_reduced_costs( + self, vars_to_load: Sequence[_GeneralVarData] | None = None + ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) else: @@ -348,11 +350,7 @@ def solve(self, model, **kwds): if config.tee: ostreams.append(sys.stdout) if config.log_solver_output: - ostreams.append( - LogStream( - level=logging.INFO, logger=logger - ) - ) + ostreams.append(LogStream(level=logging.INFO, logger=logger)) with TeeStream(*ostreams) as t: timer.start('subprocess') process = subprocess.run( @@ -484,8 +482,7 @@ def _parse_solution( res.solution_loader = SolutionLoader(None, None, None) else: res.solution_loader = ipoptSolutionLoader( - sol_data=sol_data, - nl_info=nl_info, + sol_data=sol_data, nl_info=nl_info ) return res diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index fa543d0aeaa..1fa9d653d01 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -191,9 +191,7 @@ def __init__( visibility=visibility, ) - self.solution_loader = self.declare( - 'solution_loader', ConfigValue() - ) + self.solution_loader = self.declare('solution_loader', ConfigValue()) self.termination_condition: TerminationCondition = self.declare( 'termination_condition', ConfigValue( @@ -244,7 +242,9 @@ def __init__( ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), ) - def display(self, content_filter=None, indent_spacing=2, ostream=None, visibility=0): + def display( + self, content_filter=None, indent_spacing=2, ostream=None, visibility=0 + ): return super().display(content_filter, indent_spacing, ostream, visibility) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 28fe0100015..a51f2cf9015 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -167,7 +167,9 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() var_ndx = int(suf_line[0]) - sol_data.var_suffixes[suffix_name][var_ndx] = convert_function(suf_line[1]) + sol_data.var_suffixes[suffix_name][var_ndx] = convert_function( + suf_line[1] + ) elif kind == 1: # Con sol_data.con_suffixes[suffix_name] = dict() for cnt in range(nvalues): @@ -181,7 +183,9 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() obj_ndx = int(suf_line[0]) - sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function(suf_line[1]) + sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function( + suf_line[1] + ) elif kind == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() for cnt in range(nvalues): diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 18fd96759cf..7dd882d9745 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -33,7 +33,7 @@ def load_vars( ---------- vars_to_load: list The minimum set of variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. Even if vars_to_load is specified, the values of other + to all primal variables will be loaded. Even if vars_to_load is specified, the values of other variables may also be loaded depending on the interface. """ for v, val in self.get_primals(vars_to_load=vars_to_load).items(): @@ -187,21 +187,27 @@ def load_vars( scale_list = [1] * len(self._nl_info.variables) else: scale_list = self._nl_info.scaling.variables - for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): - v.set_value(val/scale, skip_validation=True) + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): + v.set_value(val / scale, skip_validation=True) for v, v_expr in self._nl_info.eliminated_vars: v.set_value(value(v_expr), skip_validation=True) StaleFlagManager.mark_all_as_stale(delayed=True) - def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + def get_primals( + self, vars_to_load: Sequence[_GeneralVarData] | None = None + ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) else: scale_list = self._nl_info.scaling.variables val_map = dict() - for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): val_map[id(v)] = val / scale for v, v_expr in self._nl_info.eliminated_vars: @@ -211,13 +217,17 @@ def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> res = ComponentMap() if vars_to_load is None: - vars_to_load = self._nl_info.variables + [v for v, _ in self._nl_info.eliminated_vars] + vars_to_load = self._nl_info.variables + [ + v for v, _ in self._nl_info.eliminated_vars + ] for v in vars_to_load: res[v] = val_map[id(v)] return res - - def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None) -> Dict[_GeneralConstraintData, float]: + + def get_duals( + self, cons_to_load: Sequence[_GeneralConstraintData] | None = None + ) -> Dict[_GeneralConstraintData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) else: @@ -227,7 +237,9 @@ def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None else: cons_to_load = set(cons_to_load) res = dict() - for c, val, scale in zip(self._nl_info.constraints, self._sol_data.duals, scale_list): + for c, val, scale in zip( + self._nl_info.constraints, self._sol_data.duals, scale_list + ): if c in cons_to_load: res[c] = val * scale return res diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 927ab64ee12..23c2c32f819 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -83,7 +83,7 @@ def test_declared_items(self): 'termination_condition', 'timing_info', 'solver_log', - 'solver_configuration' + 'solver_configuration', } actual_declared = res._declared self.assertEqual(expected_declared, actual_declared) @@ -166,4 +166,3 @@ def test_results(self): rc2 = res.solution_loader.get_reduced_costs([m.y]) self.assertNotIn(m.x, rc2) self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - From 142dc30f8b0379be599defa8fc20d63ccf9f12c9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:44:22 -0700 Subject: [PATCH 0234/1178] Convert typing to spre-3.10 supported syntax --- pyomo/contrib/solver/solution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 7dd882d9745..33a3b1c939c 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -198,7 +198,7 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( - self, vars_to_load: Sequence[_GeneralVarData] | None = None + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) @@ -226,7 +226,7 @@ def get_primals( return res def get_duals( - self, cons_to_load: Sequence[_GeneralConstraintData] | None = None + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) From 7fd0c98ed8c1afdf2f5cbf4c24f42b7ef2aae7a8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:52:42 -0700 Subject: [PATCH 0235/1178] Add in DevError check for number of options --- pyomo/contrib/solver/sol_reader.py | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index a51f2cf9015..68654a4e9d7 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -1,12 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + from typing import Tuple, Dict, Any, List import io -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _ConstraintData -from pyomo.core.base.objective import _ObjectiveData +from pyomo.common.errors import DeveloperError from pyomo.repn.plugins.nl_writer import NLWriterInfo from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition -from pyomo.repn.plugins.nl_writer import AMPLRepn class SolFileData: @@ -44,20 +53,21 @@ def parse_sol_file( if "Options" in line: line = sol_file.readline() number_of_options = int(line) - need_tolerance = False - if ( - number_of_options > 4 - ): # MRM: Entirely unclear why this is necessary, or if it even is - number_of_options -= 2 - need_tolerance = True + # We are adding in this DeveloperError to see if the alternative case + # is ever actually hit in the wild. In a previous iteration of the sol + # reader, there was logic to check for the number of options, but it + # was uncovered by tests and unclear if actually necessary. + if number_of_options > 4: + raise DeveloperError( + """ +The sol file reader has hit an unexpected error while parsing. The number of +options recorded is greater than 4. Please report this error to the Pyomo +developers. + """ + ) for i in range(number_of_options + 4): line = sol_file.readline() model_objects.append(int(line)) - if ( - need_tolerance - ): # MRM: Entirely unclear why this is necessary, or if it even is - line = sol_file.readline() - model_objects.append(float(line)) else: raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints From f456f3736cb06b36919ec9d7834b318020cd18c8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:59:16 -0700 Subject: [PATCH 0236/1178] Missed pre-3.10 syntax error --- pyomo/contrib/solver/ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 516c0fd7f4b..1a153422eb1 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -108,7 +108,7 @@ def __init__( class ipoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( - self, vars_to_load: Sequence[_GeneralVarData] | None = None + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) From 2b1596b7ccda4b3a68747d5c6916b0c7570dae85 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 04:50:02 -0700 Subject: [PATCH 0237/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- pyomo/contrib/simplification/simplify.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89cb12d3eac..9743e45be41 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,7 +655,7 @@ jobs: run: | $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" - pytest -v pyomo/contrib/simplification/tests/test_simplification.py + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 4002f1a233f..5c0c5b859e7 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -6,7 +6,6 @@ try: from pyomo.contrib.simplification.ginac_interface import GinacInterface - ginac_available = True except: GinacInterface = None From d75c5f892a5dce5d6841c11c9dc02494c61e87b5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 04:55:31 -0700 Subject: [PATCH 0238/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9743e45be41..2384ea58e2a 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -162,9 +162,9 @@ jobs: run: | pwd cd .. - curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 - tar -xvf cln-1.3.6.tar.bz2 - cd cln-1.3.6 + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 ./configure make -j 2 sudo make install From dae02054827c2ae23d608e07a2e6913a15180631 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:00:16 -0700 Subject: [PATCH 0239/1178] run black --- pyomo/contrib/simplification/simplify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 5c0c5b859e7..4002f1a233f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -6,6 +6,7 @@ try: from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True except: GinacInterface = None From 313108d131f04e80deb59862e95099a731c84006 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:15:38 -0700 Subject: [PATCH 0240/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 2384ea58e2a..60f971c0c51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -657,7 +657,7 @@ jobs: $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ - -W ignore::Warning ${{matrix.category}} \ + ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" From 5ff4d4d3b03370580da4578e141b98d2bdaaadf0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:24:22 -0700 Subject: [PATCH 0241/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- setup.cfg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 60f971c0c51..5ed5f908d28 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/setup.cfg b/setup.cfg index 855717490b3..e8b6933bbbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,6 @@ license_files = LICENSE.md universal=1 [tool:pytest] -filterwarnings = ignore::RuntimeWarning junit_family = xunit2 markers = default: mark a test that should always run by default From 010a997ff78390af504d2dc0493cf8e96ee5fd7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:47:50 -0700 Subject: [PATCH 0242/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 9 +++------ setup.cfg | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5ed5f908d28..b766583e259 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'simplification'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -653,11 +653,8 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" - $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py - $PYTHON_EXE -m pytest -v \ - ${{matrix.category}} \ + pytest -v \ + -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" diff --git a/setup.cfg b/setup.cfg index e8b6933bbbc..855717490b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,7 @@ license_files = LICENSE.md universal=1 [tool:pytest] +filterwarnings = ignore::RuntimeWarning junit_family = xunit2 markers = default: mark a test that should always run by default From 2c59b2930d2d1b41dde9f919810a3028f248ed1d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:22:27 -0700 Subject: [PATCH 0243/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 9 +++++++-- pyomo/core/expr/compare.py | 14 +------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b766583e259..15880896961 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -653,11 +653,16 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - pytest -v \ + $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo MPI tests if: matrix.mpi != 0 run: | diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index 96913f1de39..ec8d56896b8 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -195,19 +195,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): expr2, include_named_exprs=include_named_exprs ) try: - res = True - if len(pn1) != len(pn2): - res = False - if res: - for a, b in zip(pn1, pn2): - if a.__class__ is not b.__class__: - res = False - break - if a == b: - continue - else: - res = False - break + res = pn1 == pn2 except PyomoException: res = False return res From d67d90d8c1226d0afa96ff700e173819eb822b7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:25:16 -0700 Subject: [PATCH 0244/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 15880896961..5eac447fd02 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -650,6 +650,11 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo tests if: matrix.mpi == 0 run: | @@ -658,11 +663,6 @@ jobs: pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo MPI tests if: matrix.mpi != 0 run: | From 3fcec359e1f70842af03fe9fdb8b0629785a1b64 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:31:28 -0700 Subject: [PATCH 0245/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 1 - .github/workflows/test_pr_and_main.yml | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5eac447fd02..d66451a00a5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -160,7 +160,6 @@ jobs: - name: install ginac if: matrix.other == '/singletest' run: | - pwd cd .. curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 tar -xvf cln-1.3.7.tar.bz2 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9b82c565c32..bda6014b352 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: '3.11' other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -183,9 +183,9 @@ jobs: if: matrix.other == '/singletest' run: | cd .. - curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 - tar -xvf cln-1.3.6.tar.bz2 - cd cln-1.3.6 + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 ./configure make -j 2 sudo make install @@ -671,10 +671,14 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From b5550fecb393d2ee9eeebba0e3df66b7360a10d9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:38:34 -0700 Subject: [PATCH 0246/1178] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d66451a00a5..83e652cbef8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -652,7 +652,7 @@ jobs: - name: Run Simplification Tests if: matrix.other == '/singletest' run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - name: Run Pyomo tests if: matrix.mpi == 0 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index bda6014b352..6df28fbadc9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -674,7 +674,7 @@ jobs: - name: Run Simplification Tests if: matrix.other == '/singletest' run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - name: Run Pyomo tests if: matrix.mpi == 0 From 8984389e71d57fe7b9d56c80331ea207166f1290 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:52:04 -0700 Subject: [PATCH 0247/1178] fixing simplification tests --- pyomo/core/expr/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..61ff8660a8b 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -196,7 +196,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): ) try: res = pn1 == pn2 - except PyomoException: + except (PyomoException, AttributeError): res = False return res From a49eb25f03380f6b871303d61591b0cc7be0b110 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 24 Jan 2024 13:01:06 -0500 Subject: [PATCH 0248/1178] add mindtpy call_before_subproblem_solve --- pyomo/contrib/mindtpy/algorithm_base_class.py | 13 +++++++++++++ pyomo/contrib/mindtpy/config_options.py | 9 +++++++++ pyomo/contrib/mindtpy/single_tree.py | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 7e8d390976c..f7f5e7601e5 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2958,6 +2958,10 @@ def MindtPy_iteration_loop(self): skip_fixed=False, ) if self.curr_int_sol not in set(self.integer_list): + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -2969,6 +2973,10 @@ def MindtPy_iteration_loop(self): # Solve NLP subproblem # The constraint linearization happens in the handlers if not config.solution_pool: + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -3001,6 +3009,11 @@ def MindtPy_iteration_loop(self): continue else: self.integer_list.append(self.curr_int_sol) + + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index ed0c86baae9..b6dbbedd79c 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -312,6 +312,15 @@ def _add_common_configs(CONFIG): doc='Callback hook after a solution of the main problem.', ), ) + CONFIG.declare( + 'call_before_subproblem_solve', + ConfigValue( + default=_DoNothing(), + domain=None, + description='Function to be executed after every subproblem', + doc='Callback hook after a solution of the nonlinear subproblem.', + ), + ) CONFIG.declare( 'call_after_subproblem_solve', ConfigValue( diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 228810a8f90..a82bb1ce541 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -774,6 +774,9 @@ def __call__(self): mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol) # solve subproblem + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() # add oa cuts @@ -920,6 +923,9 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): cut_ind = len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) # solve subproblem + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() From 3dc499592476144c69790c8fcead1da63958b874 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 24 Jan 2024 16:58:41 -0500 Subject: [PATCH 0249/1178] fix bug --- pyomo/contrib/mindtpy/single_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index a82bb1ce541..77740ff15e3 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -775,7 +775,7 @@ def __call__(self): # solve subproblem # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() @@ -924,7 +924,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # solve subproblem # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() From 19ed448f228647a2bb44dd47d1df1124dc14b3c0 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jan 2024 16:34:40 -0700 Subject: [PATCH 0250/1178] Finished parmest reactor_design example using new interface. --- .../reactor_design/bootstrap_example.py | 2 +- .../reactor_design/datarec_example.py | 2 +- .../reactor_design/leaveNout_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../multisensor_data_example.py | 2 +- .../parameter_estimation_example.py | 6 +- .../reactor_design/timeseries_data_example.py | 5 +- .../reactor_design/bootstrap_example.py | 32 +- .../confidence_region_example.py | 49 +++ .../reactor_design/datarec_example.py | 98 ++++- .../reactor_design/leaveNout_example.py | 29 +- .../likelihood_ratio_example.py | 32 +- .../multisensor_data_example.py | 73 +++- .../parameter_estimation_example.py | 53 +-- .../examples/reactor_design/reactor_design.py | 137 ++++-- .../reactor_design/timeseries_data_example.py | 96 +++- pyomo/contrib/parmest/experiment.py | 15 + pyomo/contrib/parmest/parmest.py | 411 +++++++++--------- 18 files changed, 647 insertions(+), 399 deletions(-) create mode 100644 pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py create mode 100644 pyomo/contrib/parmest/experiment.py diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py index e2d172f34f6..3820b78c9b1 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py @@ -12,7 +12,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py index cfd3891c00e..bae538f364c 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py @@ -12,7 +12,7 @@ import numpy as np import pandas as pd import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py index 6952a7fc733..d4ca9651753 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py @@ -13,7 +13,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py index a0fe6f22305..c47acf7f932 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py @@ -14,7 +14,7 @@ from itertools import product from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py index a92ac626fae..84c4abdf92a 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py @@ -12,7 +12,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py index 581d3904c04..67b69c73555 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py @@ -12,7 +12,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) @@ -41,7 +41,9 @@ def SSE(model, data): # Parameter estimation obj, theta = pest.theta_est() - + print (obj) + print(theta) + # Assert statements compare parameter estimation (theta) to an expected value k1_expected = 5.0 / 6.0 k2_expected = 5.0 / 3.0 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py index da2ab1874c9..e7acefc2224 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py @@ -13,9 +13,10 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) +from pyomo.contrib.parmest.deprecated.parmest import group_data def main(): @@ -31,7 +32,7 @@ def main(): # Group time series data into experiments, return the mean value for sv and caf # Returns a list of dictionaries - data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + data_ts = group_data(data, 'experiment', ['sv', 'caf']) def SSE_timeseries(model, data): expr = 0 diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index e2d172f34f6..b5cb4196456 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -13,31 +13,26 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) - def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -55,6 +50,5 @@ def SSE(model, data): title="Bootstrap theta with confidence regions", ) - if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py new file mode 100644 index 00000000000..ff84279018d --- /dev/null +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -0,0 +1,49 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + ReactorDesignExperiment, +) + +def main(): + + # Read in data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') + + # Parameter estimation + obj, theta = pest.theta_est() + + # Bootstrapping + bootstrap_theta = pest.theta_est_bootstrap(10) + print(bootstrap_theta) + + # Confidence region test + CR = pest.confidence_region_test(bootstrap_theta, "MVN", [0.5, 0.75, 1.0]) + print(CR) + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index cfd3891c00e..26185290ea6 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -11,23 +11,80 @@ import numpy as np import pandas as pd +import pyomo.environ as pyo import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( reactor_design_model, + ReactorDesignExperiment, ) np.random.seed(1234) -def reactor_design_model_for_datarec(data): +def reactor_design_model_for_datarec(): + # Unfix inlet concentration for data rec - model = reactor_design_model(data) + model = reactor_design_model() model.caf.fixed = False return model +class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): + + def __init__(self, data, data_std, experiment_number): + + super().__init__(data, experiment_number) + self.data_std = data_std + + def create_model(self): + self.model = m = reactor_design_model_for_datarec() + return m + + def label_model(self): + + m = self.model + + # experiment outputs + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) + m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) + m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) + m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + + # experiment standard deviations + m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) + m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) + m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) + m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + + # no unknowns (theta names) + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + + return m + +class ReactorDesignExperimentPostDataRec(ReactorDesignExperiment): + + def __init__(self, data, data_std, experiment_number): + + super().__init__(data, experiment_number) + self.data_std = data_std + + def label_model(self): + + m = super().label_model() + + # add experiment standard deviations + m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) + m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) + m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) + m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + + return m def generate_data(): + ### Generate data based on real sv, caf, ca, cb, cc, and cd sv_real = 1.05 caf_real = 10000 @@ -54,29 +111,34 @@ def generate_data(): def main(): + # Generate data data = generate_data() data_std = data.std() + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) + # Define sum of squared error objective function for data rec - def SSE(model, data): - expr = ( - ((float(data.iloc[0]["ca"]) - model.ca) / float(data_std["ca"])) ** 2 - + ((float(data.iloc[0]["cb"]) - model.cb) / float(data_std["cb"])) ** 2 - + ((float(data.iloc[0]["cc"]) - model.cc) / float(data_std["cc"])) ** 2 - + ((float(data.iloc[0]["cd"]) - model.cd) / float(data_std["cd"])) ** 2 - ) + def SSE(model): + expr = sum(((y - yhat)/model.experiment_outputs_std[y])**2 + for y, yhat in model.experiment_outputs.items()) return expr - ### Data reconciliation - theta_names = [] # no variables to estimate, use initialized values + # View one model & SSE + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # print(SSE(exp0_model)) - pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) + ### Data reconciliation + pest = parmest.Estimator(exp_list, obj_function=SSE) obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) print(theta) - + parmest.graphics.grouped_boxplot( data[["ca", "cb", "cc", "cd"]], data_rec[["ca", "cb", "cc", "cd"]], @@ -84,14 +146,18 @@ def SSE(model, data): ) ### Parameter estimation using reconciled data - theta_names = ["k1", "k2", "k3"] data_rec["sv"] = data["sv"] - pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) + # make a new list of experiments using reconciled data + exp_list= [] + for i in range(data_rec.shape[0]): + exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) + + pest = parmest.Estimator(exp_list, obj_function=SSE) obj, theta = pest.theta_est() print(obj) print(theta) - + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} print(theta_real) diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index 6952a7fc733..549233d8a84 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -14,19 +14,17 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create more data for the example N = 50 df_std = data.std().to_frame().transpose() @@ -34,18 +32,16 @@ def main(): df_sample = data.sample(N, replace=True).reset_index(drop=True) data = df_sample + df_rand.dot(df_std) / 10 - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -93,6 +89,5 @@ def SSE(model, data): percent_true = sum(r) / len(r) print(percent_true) - if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index a0fe6f22305..8b6d9fcfecc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -15,31 +15,27 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data + +# Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index a92ac626fae..f731032368e 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -11,37 +11,76 @@ import pandas as pd from os.path import join, abspath, dirname +import pyomo.environ as pyo import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) +class MultisensorReactorDesignExperiment(ReactorDesignExperiment): + + def finalize_model(self): + + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1/3) + m.cb = self.data_i['cb'] + m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1/2) + m.cd = self.data_i['cd'] + + return m + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])]) + m.experiment_outputs.update([(m.cb, [self.data_i['cb']])]) + m.experiment_outputs.update([(m.cc, [self.data_i['cc1'], self.data_i['cc2']])]) + m.experiment_outputs.update([(m.cd, [self.data_i['cd']])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k1, m.k2, m.k3]) + + return m + + def main(): # Parameter estimation using multisensor data - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data, includes multiple sensors for ca and cc + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) data = pd.read_csv(file_name) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(MultisensorReactorDesignExperiment(data, i)) - # Sum of squared error function - def SSE_multisensor(model, data): - expr = ( - ((float(data.iloc[0]["ca1"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca2"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca3"]) - model.ca) ** 2) * (1 / 3) - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + ((float(data.iloc[0]["cc1"]) - model.cc) ** 2) * (1 / 2) - + ((float(data.iloc[0]["cc2"]) - model.cc) ** 2) * (1 / 2) - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) + # Define sum of squared error + def SSE_multisensor(model): + expr = 0 + for y, yhat in model.experiment_outputs.items(): + num_outputs = len(yhat) + for i in range(num_outputs): + expr += ((y - yhat[i])**2) * (1 / num_outputs) return expr + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # print(SSE_multisensor(exp0_model)) - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) + pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 581d3904c04..76744984cce 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -13,46 +13,29 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Assert statements compare parameter estimation (theta) to an expected value - k1_expected = 5.0 / 6.0 - k2_expected = 5.0 / 3.0 - k3_expected = 1.0 / 6000.0 - relative_error = abs(theta["k1"] - k1_expected) / k1_expected - assert relative_error < 0.05 - relative_error = abs(theta["k2"] - k2_expected) / k2_expected - assert relative_error < 0.05 - relative_error = abs(theta["k3"] - k3_expected) / k3_expected - assert relative_error < 0.05 - - -if __name__ == "__main__": - main() + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') + + # Parameter estimation with covariance + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) + print(obj) + print(theta) \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 16f65e236eb..1479009abcc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -12,57 +12,42 @@ Continuously stirred tank reactor model, based on pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py """ +from os.path import join, abspath, dirname +from itertools import product import pandas as pd -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - maximize, - SolverFactory, -) - - -def reactor_design_model(data): + +import pyomo.environ as pyo +import pyomo.contrib.parmest.parmest as parmest + +from pyomo.contrib.parmest.experiment import Experiment + +def reactor_design_model(): + # Create the concrete model - model = ConcreteModel() + model = pyo.ConcreteModel() # Rate constants - model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 - model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 - model.k3 = Param( - initialize=1.0 / 6000.0, within=PositiveReals, mutable=True - ) # m^3/(gmol min) + model.k1 = pyo.Param(initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True) # min^-1 + model.k2 = pyo.Param(initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True) # min^-1 + model.k3 = pyo.Param(initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True) # m^3/(gmol min) # Inlet concentration of A, gmol/m^3 - if isinstance(data, dict) or isinstance(data, pd.Series): - model.caf = Param(initialize=float(data["caf"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.caf = Param(initialize=float(data.iloc[0]["caf"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") - + model.caf = pyo.Param(initialize=10000, within=pyo.PositiveReals, mutable=True) + # Space velocity (flowrate/volume) - if isinstance(data, dict) or isinstance(data, pd.Series): - model.sv = Param(initialize=float(data["sv"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.sv = Param(initialize=float(data.iloc[0]["sv"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") + model.sv = pyo.Param(initialize=1.0, within=pyo.PositiveReals, mutable=True) # Outlet concentration of each component - model.ca = Var(initialize=5000.0, within=PositiveReals) - model.cb = Var(initialize=2000.0, within=PositiveReals) - model.cc = Var(initialize=2000.0, within=PositiveReals) - model.cd = Var(initialize=1000.0, within=PositiveReals) + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) # Objective - model.obj = Objective(expr=model.cb, sense=maximize) + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) # Constraints - model.ca_bal = Constraint( + model.ca_bal = pyo.Constraint( expr=( 0 == model.sv * model.caf @@ -72,33 +57,89 @@ def reactor_design_model(data): ) ) - model.cb_bal = Constraint( + model.cb_bal = pyo.Constraint( expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) ) - model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + model.cc_bal = pyo.Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) - model.cd_bal = Constraint( + model.cd_bal = pyo.Constraint( expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) return model + +class ReactorDesignExperiment(Experiment): + + def __init__(self, data, experiment_number): + self.data = data + self.experiment_number = experiment_number + self.data_i = data.loc[experiment_number,:] + self.model = None + + def create_model(self): + self.model = m = reactor_design_model() + return m + + def finalize_model(self): + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = self.data_i['ca'] + m.cb = self.data_i['cb'] + m.cc = self.data_i['cc'] + m.cd = self.data_i['cd'] + + return m + + def label_model(self): + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) + m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) + m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) + m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k1, m.k2, m.k3]) + + return m + + def get_labeled_model(self): + m = self.create_model() + m = self.finalize_model() + m = self.label_model() + + return m +if __name__ == "__main__": -def main(): # For a range of sv values, return ca, cb, cc, and cd results = [] sv_values = [1.0 + v * 0.05 for v in range(1, 20)] caf = 10000 for sv in sv_values: - model = reactor_design_model(pd.DataFrame(data={"caf": [caf], "sv": [sv]})) - solver = SolverFactory("ipopt") + + # make model + model = reactor_design_model() + + # add caf, sv + model.caf = caf + model.sv = sv + + # solve model + solver = pyo.SolverFactory("ipopt") solver.solve(model) + + # save results results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) - - -if __name__ == "__main__": - main() + \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index da2ab1874c9..a9a5ab20b54 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -14,16 +14,74 @@ import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) +class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): + + def __init__(self, data, experiment_number): + self.data = data + self.experiment_number = experiment_number + self.data_i = data[experiment_number] + self.model = None + + def finalize_model(self): + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = self.data_i['ca'][0] + m.cb = self.data_i['cb'][0] + m.cc = self.data_i['cc'][0] + m.cd = self.data_i['cd'][0] + + return m + + +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + def main(): # Parameter estimation using timeseries data - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - # Data, includes multiple sensors for ca and cc file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) @@ -31,21 +89,29 @@ def main(): # Group time series data into experiments, return the mean value for sv and caf # Returns a list of dictionaries - data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + data_ts = group_data(data, 'experiment', ['sv', 'caf']) + + # Create an experiment list + exp_list= [] + for i in range(len(data_ts)): + exp_list.append(TimeSeriesReactorDesignExperiment(data_ts, i)) + + def SSE_timeseries(model): - def SSE_timeseries(model, data): expr = 0 - for val in data['ca']: - expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) - for val in data['cb']: - expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) - for val in data['cc']: - expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) - for val in data['cd']: - expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) + for y, yhat in model.experiment_outputs.items(): + num_time_points = len(yhat) + for i in range(num_time_points): + expr += ((y - yhat[i])**2) * (1 / num_time_points) + return expr - pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) + # View one model & SSE + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # print(SSE_timeseries(exp0_model)) + + pest = parmest.Estimator(exp_list, obj_function=SSE_timeseries) obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py new file mode 100644 index 00000000000..73b18bb5975 --- /dev/null +++ b/pyomo/contrib/parmest/experiment.py @@ -0,0 +1,15 @@ +# The experiment class is a template for making experiment lists +# to pass to parmest. An experiment is a pyomo model "m" which has +# additional suffixes: +# m.experiment_outputs -- which variables are experiment outputs +# m.unknown_parameters -- which variables are parameters to estimate +# The experiment class has only one required method: +# get_labeled_model() +# which returns the labeled pyomo model. + +class Experiment: + def __init__(self, model=None): + self.model = model + + def get_labeled_model(self): + return self.model \ No newline at end of file diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 1f9b8b645b8..a00671c2ea6 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -11,9 +11,22 @@ #### Using mpi-sppy instead of PySP; May 2020 #### Adding option for "local" EF starting Sept 2020 #### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 +#### Redesign with Experiment class Dec 2023 # TODO: move use_mpisppy to a Pyomo configuration option -# + +# Redesign TODOS +# TODO: remove group_data,this is only used in 1 example and should be handled by the user in Experiment +# TODO: _treemaker is not used in parmest, the code could be moved to scenario tree if needed +# TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) +# TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) +# TODO: Clean up the use of updated_theta_names, model_theta_names, estimator_theta_names. Not sure if estimator_theta_names is the union or intersect of thetas in each model +# TODO: _return_theta_names should no longer be needed +# TODO: generally, theta ordering is not preserved by pyomo, so we should check that ordering +# matches values for each function, otherwise results will be wrong and/or inconsistent +# TODO: return model object (m.k1) and CUIDs in dataframes instead of names ("k1") + + # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: @@ -224,90 +237,92 @@ def _experiment_instance_creation_callback( return instance -# ============================================= -def _treemaker(scenlist): - """ - Makes a scenario tree (avoids dependence on daps) - - Parameters - ---------- - scenlist (list of `int`): experiment (i.e. scenario) numbers - - Returns - ------- - a `ConcreteModel` that is the scenario tree - """ - - num_scenarios = len(scenlist) - m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() - m = m.create_instance() - m.Stages.add('Stage1') - m.Stages.add('Stage2') - m.Nodes.add('RootNode') - for i in scenlist: - m.Nodes.add('LeafNode_Experiment' + str(i)) - m.Scenarios.add('Experiment' + str(i)) - m.NodeStage['RootNode'] = 'Stage1' - m.ConditionalProbability['RootNode'] = 1.0 - for node in m.Nodes: - if node != 'RootNode': - m.NodeStage[node] = 'Stage2' - m.Children['RootNode'].add(node) - m.Children[node].clear() - m.ConditionalProbability[node] = 1.0 / num_scenarios - m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - - return m - - -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - +# # ============================================= +# def _treemaker(scenlist): +# """ +# Makes a scenario tree (avoids dependence on daps) + +# Parameters +# ---------- +# scenlist (list of `int`): experiment (i.e. scenario) numbers + +# Returns +# ------- +# a `ConcreteModel` that is the scenario tree +# """ + +# num_scenarios = len(scenlist) +# m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() +# m = m.create_instance() +# m.Stages.add('Stage1') +# m.Stages.add('Stage2') +# m.Nodes.add('RootNode') +# for i in scenlist: +# m.Nodes.add('LeafNode_Experiment' + str(i)) +# m.Scenarios.add('Experiment' + str(i)) +# m.NodeStage['RootNode'] = 'Stage1' +# m.ConditionalProbability['RootNode'] = 1.0 +# for node in m.Nodes: +# if node != 'RootNode': +# m.NodeStage[node] = 'Stage2' +# m.Children['RootNode'].add(node) +# m.Children[node].clear() +# m.ConditionalProbability[node] = 1.0 / num_scenarios +# m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node + +# return m + + +# def group_data(data, groupby_column_name, use_mean=None): +# """ +# Group data by scenario + +# Parameters +# ---------- +# data: DataFrame +# Data +# groupby_column_name: strings +# Name of data column which contains scenario numbers +# use_mean: list of column names or None, optional +# Name of data columns which should be reduced to a single value per +# scenario by taking the mean + +# Returns +# ---------- +# grouped_data: list of dictionaries +# Grouped data +# """ +# if use_mean is None: +# use_mean_list = [] +# else: +# use_mean_list = use_mean + +# grouped_data = [] +# for exp_num, group in data.groupby(data[groupby_column_name]): +# d = {} +# for col in group.columns: +# if col in use_mean_list: +# d[col] = group[col].mean() +# else: +# d[col] = list(group[col]) +# grouped_data.append(d) + +# return grouped_data + +def SSE(model): + expr = sum((y - yhat)**2 for y, yhat in model.experiment_outputs.items()) + return expr class _SecondStageCostExpr(object): """ Class to pass objective expression into the Pyomo model """ - def __init__(self, ssc_function, data): + def __init__(self, ssc_function): self._ssc_function = ssc_function - self._data = data def __call__(self, model): - return self._ssc_function(model, self._data) + return self._ssc_function(model) class Estimator(object): @@ -316,17 +331,12 @@ class Estimator(object): Parameters ---------- - model_function: function - Function that generates an instance of the Pyomo model using 'data' - as the input argument - data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names - Data that is used to build an instance of the Pyomo model and build - the objective function - theta_names: list of strings - List of Var names to estimate - obj_function: function, optional - Function used to formulate parameter estimation objective, generally - sum of squared error between measurements and model variables. + experiement_list: list of Experiments + A list of experiment objects which creates one labeled model for + each expeirment + obj_function: string or function (optional) + Built in objective (currently only "SSE") or custom function used to + formulate parameter estimation objective. If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. @@ -342,54 +352,49 @@ class Estimator(object): # from parmest_deprecated as well as the new inputs using experiment lists def __init__(self, *args, **kwargs): + # check that we have at least one argument + assert(len(args) > 0) + # use deprecated interface self.pest_deprecated = None - if len(args) > 1: + if callable(args[0]): logger.warning('Using deprecated parmest inputs (model_function, ' + 'data, theta_names), please use experiment lists instead.') self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) return - print("New parmest interface using Experiment lists coming soon!") - exit() - - # def __init__( - # self, - # model_function, - # data, - # theta_names, - # obj_function=None, - # tee=False, - # diagnostic_mode=False, - # solver_options=None, - # ): - - self.model_function = model_function - - assert isinstance( - data, (list, pd.DataFrame) - ), "Data must be a list or DataFrame" - # convert dataframe into a list of dataframes, each row = one scenario - if isinstance(data, pd.DataFrame): - self.callback_data = [ - data.loc[i, :].to_frame().transpose() for i in data.index - ] - else: - self.callback_data = data - assert isinstance( - self.callback_data[0], (dict, pd.DataFrame, str) - ), "The scenarios in data must be a dictionary, DataFrame or filename" - - if len(theta_names) == 0: - self.theta_names = ['parmest_dummy_var'] - else: - self.theta_names = theta_names - - self.obj_function = obj_function - self.tee = tee - self.diagnostic_mode = diagnostic_mode - self.solver_options = solver_options + # check that we have a (non-empty) list of experiments + assert (isinstance(args[0], list)) + assert (len(args[0]) > 0) + self.exp_list = args[0] + # check that an experiment has experiment_outputs and unknown_parameters + model = self.exp_list[0].get_labeled_model() + try: + outputs = [k.name for k,v in model.experiment_outputs.items()] + except: + RuntimeError('Experiment list model does not have suffix ' + + '"experiment_outputs".') + try: + parms = [k.name for k,v in model.unknown_parameters.items()] + except: + RuntimeError('Experiment list model does not have suffix ' + + '"unknown_parameters".') + + # populate keyword argument options + self.obj_function = kwargs.get('obj_function', None) + self.tee = kwargs.get('tee', False) + self.diagnostic_mode = kwargs.get('diagnostic_mode', False) + self.solver_options = kwargs.get('solver_options', None) + + # TODO This might not be needed here. + # We could collect the union (or intersect?) of thetas when the models are built + theta_names = [] + for experiment in self.exp_list: + model = experiment.get_labeled_model() + theta_names.extend([k.name for k,v in model.unknown_parameters.items()]) + self.estimator_theta_names = list(set(theta_names)) + self._second_stage_cost_exp = "SecondStageCost" # boolean to indicate if model is initialized using a square solve self.model_initialized = False @@ -412,17 +417,26 @@ def _return_theta_names(self): ) # default theta_names, created when Estimator object is created else: - return None - def _create_parmest_model(self, data): + # if fitted model parameter names differ from theta_names + # created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.estimator_theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, experiment_number): """ Modify the Pyomo model for parameter estimation """ - model = self.model_function(data) - if (len(self.theta_names) == 1) and ( - self.theta_names[0] == 'parmest_dummy_var' - ): + model = self.exp_list[experiment_number].get_labeled_model() + self.theta_names = [k.name for k,v in model.unknown_parameters.items()] + + if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) # Add objective function (optional) @@ -441,10 +455,17 @@ def _create_parmest_model(self, data): "Parmest will not override the existing model Expression named " + expr.name ) + + # TODO, this needs to be turned a enum class of options that still support custom functions + if self.obj_function == 'SSE': + second_stage_rule=_SecondStageCostExpr(SSE) + else: + # A custom function uses model.experiment_outputs as data + second_stage_rule = _SecondStageCostExpr(self.obj_function) + model.FirstStageCost = pyo.Expression(expr=0) - model.SecondStageCost = pyo.Expression( - rule=_SecondStageCostExpr(self.obj_function, data) - ) + model.SecondStageCost = pyo.Expression(rule=second_stage_rule) + def TotalCost_rule(model): return model.FirstStageCost + model.SecondStageCost @@ -479,20 +500,7 @@ def TotalCost_rule(model): return model def _instance_creation_callback(self, experiment_number=None, cb_data=None): - # cb_data is a list of dictionaries, list of dataframes, OR list of json file names - exp_data = cb_data[experiment_number] - if isinstance(exp_data, (dict, pd.DataFrame)): - pass - elif isinstance(exp_data, str): - try: - with open(exp_data, 'r') as infile: - exp_data = json.load(infile) - except: - raise RuntimeError(f'Could not read {exp_data} as json') - else: - raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') - model = self._create_parmest_model(exp_data) - + model = self._create_parmest_model(experiment_number) return model def _Q_opt( @@ -514,7 +522,7 @@ def _Q_opt( # (Bootstrap scenarios will use indirection through the bootlist) if bootlist is None: - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) scen_names = ["Scenario{}".format(i) for i in scenario_numbers] else: scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] @@ -526,8 +534,8 @@ def _Q_opt( outer_cb_data["ThetaVals"] = ThetaVals if bootlist is not None: outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = self.callback_data # None is OK - outer_cb_data["theta_names"] = self.theta_names + outer_cb_data["cb_data"] = None # None is OK + outer_cb_data["theta_names"] = self.estimator_theta_names options = {"solver": "ipopt"} scenario_creator_options = {"cb_data": outer_cb_data} @@ -702,13 +710,13 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): "callback": self._instance_creation_callback, "ThetaVals": thetavals, "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, + "cb_data": None, } else: dummy_cb = { "callback": self._instance_creation_callback, "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, + "cb_data": None, } if self.diagnostic_mode: @@ -729,7 +737,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): WorstStatus = pyo.TerminationCondition.optimal totobj = 0 - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) if initialize_parmest_model: # create dictionary to store pyomo model instances (scenarios) scen_dict = dict() @@ -737,13 +745,14 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): for snum in scenario_numbers: sname = "scenario_NODE" + str(snum) instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + model_theta_names = [k.name for k,v in instance.unknown_parameters.items()] if initialize_parmest_model: # list to store fitted parameter names that will be unfixed # after initialization theta_init_vals = [] # use appropriate theta_names member - theta_ref = self._return_theta_names() + theta_ref = model_theta_names for i, theta in enumerate(theta_ref): # Use parser in ComponentUID to locate the component @@ -868,7 +877,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): def _get_sample_list(self, samplesize, num_samples, replacement=True): samplelist = list() - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) if num_samples is None: # This could get very large @@ -944,12 +953,12 @@ def theta_est( assert isinstance(return_values, list) assert isinstance(calc_cov, bool) if calc_cov: - assert isinstance( - cov_n, int - ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" - assert cov_n > len( - self._return_theta_names() - ), "The number of datapoints must be greater than the number of parameters to estimate" + num_unknowns = max([len(experiment.get_labeled_model().unknown_parameters) + for experiment in self.exp_list]) + assert isinstance(cov_n, int), \ + "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert cov_n > num_unknowns, \ + "The number of datapoints must be greater than the number of parameters to estimate" return self._Q_opt( solver=solver, @@ -1007,7 +1016,7 @@ def theta_est_bootstrap( assert isinstance(return_samples, bool) if samplesize is None: - samplesize = len(self.callback_data) + samplesize = len(self.exp_list) if seed is not None: np.random.seed(seed) @@ -1069,7 +1078,7 @@ def theta_est_leaveNout( assert isinstance(seed, (type(None), int)) assert isinstance(return_samples, bool) - samplesize = len(self.callback_data) - lNo + samplesize = len(self.exp_list) - lNo if seed is not None: np.random.seed(seed) @@ -1082,7 +1091,7 @@ def theta_est_leaveNout( lNo_theta = list() for idx, sample in local_list: objval, thetavals = self._Q_opt(bootlist=list(sample)) - lNo_s = list(set(range(len(self.callback_data))) - set(sample)) + lNo_s = list(set(range(len(self.exp_list))) - set(sample)) thetavals['lNo'] = np.sort(lNo_s) lNo_theta.append(thetavals) @@ -1157,20 +1166,13 @@ def leaveNout_bootstrap_test( if seed is not None: np.random.seed(seed) - data = self.callback_data.copy() - global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) results = [] for idx, sample in global_list: - # Reset callback_data to only include the sample - self.callback_data = [data[i] for i in sample] obj, theta = self.theta_est() - # Reset callback_data to include all scenarios except the sample - self.callback_data = [data[i] for i in range(len(data)) if i not in sample] - bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) training, test = self.confidence_region_test( @@ -1182,9 +1184,6 @@ def leaveNout_bootstrap_test( results.append((sample, test, training)) - # Reset callback_data (back to full data set) - self.callback_data = data - return results def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): @@ -1214,37 +1213,39 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): theta_values=theta_values, initialize_parmest_model=initialize_parmest_model) - if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': + if len(self.estimator_theta_names) == 0: pass # skip assertion if model has no fitted parameters else: # create a local instance of the pyomo model to access model variables and parameters - model_temp = self._create_parmest_model(self.callback_data[0]) - model_theta_list = [] # list to store indexed and non-indexed parameters - # iterate over original theta_names - for theta_i in self.theta_names: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - # check if theta in theta_names are indexed - try: - # get component UID of Set over which theta is defined - set_cuid = ComponentUID(var_validate.index_set()) - # access and iterate over the Set to generate theta names as they appear - # in the pyomo model - set_validate = set_cuid.find_component_on(model_temp) - for s in set_validate: - self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" - # generate list of theta names - model_theta_list.append(self_theta_temp) - # if theta is not indexed, copy theta name to list as-is - except AttributeError: - self_theta_temp = repr(var_cuid) - model_theta_list.append(self_theta_temp) - except: - raise + model_temp = self._create_parmest_model(0) + model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] + + # # iterate over original theta_names + # for theta_i in self.theta_names: + # var_cuid = ComponentUID(theta_i) + # var_validate = var_cuid.find_component_on(model_temp) + # # check if theta in theta_names are indexed + # try: + # # get component UID of Set over which theta is defined + # set_cuid = ComponentUID(var_validate.index_set()) + # # access and iterate over the Set to generate theta names as they appear + # # in the pyomo model + # set_validate = set_cuid.find_component_on(model_temp) + # for s in set_validate: + # self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" + # # generate list of theta names + # model_theta_list.append(self_theta_temp) + # # if theta is not indexed, copy theta name to list as-is + # except AttributeError: + # self_theta_temp = repr(var_cuid) + # model_theta_list.append(self_theta_temp) + # except: + # raise + # if self.theta_names is not the same as temp model_theta_list, # create self.theta_names_updated - if set(self.theta_names) == set(model_theta_list) and len( - self.theta_names + if set(self.estimator_theta_names) == set(model_theta_list) and len( + self.estimator_theta_names ) == set(model_theta_list): pass else: @@ -1253,7 +1254,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member - theta_names = self._return_theta_names() + theta_names = self.estimator_theta_names() else: assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop @@ -1343,7 +1344,7 @@ def likelihood_ratio_test( assert isinstance(return_thresholds, bool) LR = obj_at_theta.copy() - S = len(self.callback_data) + S = len(self.exp_list) thresholds = {} for a in alphas: chi2_val = scipy.stats.chi2.ppf(a, 2) From 280cd8027510b89118b13417634d21b411a93597 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 25 Jan 2024 13:25:15 -0700 Subject: [PATCH 0251/1178] Fixed parmest reaction kinetics example to work with new interface. --- .../simple_reaction_parmest_example.py | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 719a930251c..140fceeb8a2 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -18,6 +18,7 @@ Code provided by Paul Akula. ''' +import pyomo.environ as pyo from pyomo.environ import ( ConcreteModel, Param, @@ -32,6 +33,7 @@ value, ) import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.experiment import Experiment def simple_reaction_model(data): @@ -72,7 +74,62 @@ def total_cost_rule(m): return model +# For this experiment class, data is dictionary +class SimpleReactionExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = simple_reaction_model(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.x1, self.data['x1'])]) + m.experiment_outputs.update([(m.x2, self.data['x2'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) + + return m + + def get_labeled_model(self): + self.create_model() + m = self.label_model() + + return m + +# k[2] fixed +class SimpleReactionExperimentK2Fixed(SimpleReactionExperiment): + + def label_model(self): + + m = super().label_model() + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k[1]]) + + return m + +# k[2] variable +class SimpleReactionExperimentK2Variable(SimpleReactionExperiment): + + def label_model(self): + + m = super().label_model() + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k[1], m.k[2]]) + + return m + + def main(): + # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) data = [ {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, @@ -92,21 +149,34 @@ def main(): {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, ] + # Create an experiment list with k[2] fixed + exp_list= [] + for i in range(len(data)): + exp_list.append(SimpleReactionExperimentK2Fixed(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # ======================================================================= # Parameter estimation without covariance estimate # Only estimate the parameter k[1]. The parameter k[2] will remain fixed # at its initial value - theta_names = ['k[1]'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) + + pest = parmest.Estimator(exp_list) obj, theta = pest.theta_est() print(obj) print(theta) print() + # Create an experiment list with k[2] variable + exp_list= [] + for i in range(len(data)): + exp_list.append(SimpleReactionExperimentK2Variable(data[i])) + # ======================================================================= # Estimate both k1 and k2 and compute the covariance matrix - theta_names = ['k'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) + pest = parmest.Estimator(exp_list) n = 15 # total number of data points used in the objective (y in 15 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) print(obj) From b6395abfbc6db5198e04f15c361c745f7441a5af Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 26 Jan 2024 08:27:12 -0700 Subject: [PATCH 0252/1178] new ipopt interface: account for parameters in objective when load_solution=False --- pyomo/contrib/solver/ipopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 1a153422eb1..48c314ffc79 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -420,7 +420,7 @@ def solve(self, model, **kwds): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: - results.incumbent_objective = replace_expressions( + results.incumbent_objective = value(replace_expressions( nl_info.objectives[0].expr, substitution_map={ id(v): val @@ -428,7 +428,7 @@ def solve(self, model, **kwds): }, descend_into_named_expressions=True, remove_named_expressions=True, - ) + )) results.solver_configuration = config results.solver_log = ostreams[0].getvalue() From f126bc4520cb6edd76219256e4df87e88a372e6d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 26 Jan 2024 08:52:39 -0700 Subject: [PATCH 0253/1178] Apply black --- pyomo/contrib/solver/ipopt.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 48c314ffc79..534a9173d07 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -420,15 +420,17 @@ def solve(self, model, **kwds): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: - results.incumbent_objective = value(replace_expressions( - nl_info.objectives[0].expr, - substitution_map={ - id(v): val - for v, val in results.solution_loader.get_primals().items() - }, - descend_into_named_expressions=True, - remove_named_expressions=True, - )) + results.incumbent_objective = value( + replace_expressions( + nl_info.objectives[0].expr, + substitution_map={ + id(v): val + for v, val in results.solution_loader.get_primals().items() + }, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) + ) results.solver_configuration = config results.solver_log = ostreams[0].getvalue() From c7caf805f93d206f14f3336b3f9f39e2c8862cec Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jan 2024 09:20:38 -0700 Subject: [PATCH 0254/1178] Added parmest rooney-biegler example wtih new interface. --- .../rooney_biegler/bootstrap_example.py | 22 ++++++---- .../likelihood_ratio_example.py | 22 ++++++---- .../parameter_estimation_example.py | 24 ++++++---- .../examples/rooney_biegler/rooney_biegler.py | 44 ++++++++++++++++++- .../rooney_biegler_with_constraint.py | 42 ++++++++++++++++++ 5 files changed, 128 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index f686bbd933d..1f15ab95779 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -12,13 +12,11 @@ import pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -27,14 +25,22 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index 5e54a33abda..869bb39efb9 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -14,13 +14,11 @@ from itertools import product import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -29,14 +27,22 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 9af33217fe4..b6ca7af0ab6 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -12,13 +12,11 @@ import pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -27,15 +25,23 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Create an instance of the parmest estimator + pest = parmest.Estimator(exp_list, obj_function=SSE) + # Parameter estimation and covariance n = 6 # total number of data points used in the objective (y in 6 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 5a0e1238e85..2ac03504260 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -17,6 +17,7 @@ import pandas as pd import pyomo.environ as pyo +from pyomo.contrib.parmest.experiment import Experiment def rooney_biegler_model(data): @@ -25,10 +26,13 @@ def rooney_biegler_model(data): model.asymptote = pyo.Var(initialize=15) model.rate_constant = pyo.Var(initialize=0.5) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr - + model.response_function = pyo.Expression(data.hour, rule=response_rule) def SSE_rule(m): @@ -41,6 +45,44 @@ def SSE_rule(m): return model +class RooneyBieglerExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = rooney_biegler_model(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.asymptote, m.rate_constant]) + + def finalize_model(self): + + m = self.model + + # Experiment output values + m.hour = self.data.iloc[0]['hour'] + m.y = self.data.iloc[0]['y'] + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # These were taken from Table A1.4 in Bates and Watts (1988). data = pd.DataFrame( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 2582e3fe928..1e213684a01 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -17,6 +17,7 @@ import pandas as pd import pyomo.environ as pyo +from pyomo.contrib.parmest.experiment import Experiment def rooney_biegler_model_with_constraint(data): @@ -24,6 +25,10 @@ def rooney_biegler_model_with_constraint(data): model.asymptote = pyo.Var(initialize=15) model.rate_constant = pyo.Var(initialize=0.5) + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.response_function = pyo.Var(data.hour, initialize=0.0) # changed from expression to constraint @@ -43,6 +48,43 @@ def SSE_rule(m): return model +class RooneyBieglerExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = rooney_biegler_model_with_constraint(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.asymptote, m.rate_constant]) + + def finalize_model(self): + + m = self.model + + # Experiment output values + m.hour = self.data.iloc[0]['hour'] + m.y = self.data.iloc[0]['y'] + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + def main(): # These were taken from Table A1.4 in Bates and Watts (1988). From 4260d193bfeb9f2bd6616395824789cbdc637a92 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Jan 2024 15:33:58 -0700 Subject: [PATCH 0255/1178] Adding 'spans' and 'alternative' expressions --- pyomo/contrib/cp/__init__.py | 1 + pyomo/contrib/cp/interval_var.py | 9 +++- pyomo/contrib/cp/repn/docplex_writer.py | 19 ++++++++ .../cp/scheduling_expr/scheduling_logic.py | 48 +++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/cp/scheduling_expr/scheduling_logic.py diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index 03196537446..bd839f5d578 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -14,6 +14,7 @@ before_in_sequence, predecessor_to, ) +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import alternative, spans from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 911d9ba50ba..2379a58a1ff 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -11,6 +11,7 @@ from pyomo.common.collections import ComponentSet from pyomo.common.pyomo_typing import overload +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import SpanExpression from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, @@ -24,6 +25,7 @@ from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set from pyomo.core.base.initializer import BoundInitializer, Initializer from pyomo.core.expr import GetItemExpression +from pyomo.core.expr.logical_expr import _flattened class IntervalVarTimePoint(ScalarVar): @@ -80,7 +82,9 @@ class IntervalVarPresence(ScalarBooleanVar): __slots__ = () - def __init__(self): + def __init__(self, *args, **kwd): + # TODO: adding args and kwd above made Reference work, but we + # probably shouldn't just swallow them, right? super().__init__(ctype=IntervalVarPresence) def get_associated_interval_var(self): @@ -122,6 +126,9 @@ def optional(self, val): else: self.is_present.fix(True) + def spans(self, *args): + return SpanExpression([self] + list(_flattened(args))) + @ModelComponentFactory.register("Interval variables for scheduling.") class IntervalVar(Block): diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index de71e4e98dd..1f6bcc347e7 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -36,6 +36,10 @@ IndexedSequenceVar, _SequenceVarData, ) +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + AlternativeExpression, + SpanExpression, +) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, @@ -462,6 +466,7 @@ def _create_docplex_interval_var(visitor, interval_var): nm = interval_var.name if visitor.symbolic_solver_labels else None cpx_interval_var = cp.interval_var(name=nm) visitor.var_map[id(interval_var)] = cpx_interval_var + visitor.pyomo_to_docplex[interval_var] = cpx_interval_var # Figure out if it exists if interval_var.is_present.fixed and not interval_var.is_present.value: @@ -991,6 +996,18 @@ def _handle_predecessor_to_expression_node( return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) +def _handle_span_expression_node( + visitor, node, *args +): + return _GENERAL, cp.span(args[0][1], [arg[1] for arg in args[1:]]) + + +def _handle_alternative_expression_node( + visitor, node, *args +): + return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) + + class LogicalToDoCplex(StreamBasedExpressionVisitor): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, @@ -1037,6 +1054,8 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): LastInSequenceExpression: _handle_last_in_sequence_expression_node, BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, PredecessorToExpression: _handle_predecessor_to_expression_node, + SpanExpression: _handle_span_expression_node, + AlternativeExpression: _handle_alternative_expression_node, } _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py new file mode 100644 index 00000000000..a1f891a769f --- /dev/null +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -0,0 +1,48 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from pyomo.core.expr.logical_expr import NaryBooleanExpression, _flattened + + +class SpanExpression(NaryBooleanExpression): + """ + Expression over IntervalVars representing that the first arg spans all the + following args in the schedule. The first arg is absent if and only if all + the others are absent. + + args: + args (tuple): Child nodes, of type IntervalVar + """ + def _to_string(self, values, verbose, smap): + return "%s.spans(%s)" % (values[0], ", ".join(values[1:])) + + +class AlternativeExpression(NaryBooleanExpression): + """ + TODO/ + """ + def _to_string(self, values, verbose, smap): + return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) + + +def spans(*args): + """Creates a new SpanExpression + """ + + return SpanExpression(list(_flattened(args))) + + +def alternative(*args): + """Creates a new AlternativeExpression + """ + + return AlternativeExpression(list(_flattened(args))) From db4cb7dcc9e78471d9e3353ba75b6e79fa3d1651 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Jan 2024 16:21:53 -0700 Subject: [PATCH 0256/1178] Adding tests for span and alternative --- pyomo/contrib/cp/tests/test_docplex_walker.py | 52 ++++++++++++++++++ .../cp/tests/test_sequence_expressions.py | 53 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 0b2057217c0..fc475190ade 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -19,6 +19,7 @@ last_in_sequence, before_in_sequence, predecessor_to, + alternative ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, @@ -1322,6 +1323,57 @@ def param_rule(m, i): self.assertTrue(expr[1].equals(cp.element([2, 4, 6], 0 + 1 * (x - 1) // 2) / a)) +@unittest.skipIf(not docplex_available, "docplex is not available") +class TestCPExpressionWalker_HierarchicalScheduling(CommonTest): + def get_model(self): + m = ConcreteModel() + def start_rule(m, i): + return 2*i + def length_rule(m, i): + return i + m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, + optional=True) + m.whole_enchilada = IntervalVar() + + return m + + def test_spans(self): + m = self.get_model() + e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue(expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in + [1, 2, 3]]))) + + def test_alternative(self): + m = self.get_model() + e = alternative(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue(expr[1].equals(cp.alternative(whole_enchilada, [iv[i] + for i in + [1, 2, + 3]]))) + + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_CumulFuncExpressions(CommonTest): def test_always_in(self): diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 0ef2a9e3072..93a283c43d1 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -12,6 +12,12 @@ from io import StringIO import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + AlternativeExpression, + SpanExpression, + alternative, + spans +) from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( NoOverlapExpression, FirstInSequenceExpression, @@ -102,3 +108,50 @@ def test_predecessor_in_sequence(self): self.assertIs(e.args[2], m.seq) self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") + + +class TestHierarchicalSchedulingExpressions(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + def start_rule(m, i): + return 2*i + def length_rule(m, i): + return i + m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, + optional=True) + m.whole_enchilada = IntervalVar() + + return m + + def check_span_expression(self, m, e): + self.assertIsInstance(e, SpanExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "whole_enchilada.spans(iv[1], iv[2], iv[3])") + + def test_spans(self): + m = self.make_model() + e = spans(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + self.check_span_expression(m, e) + + def test_spans_method(self): + m = self.make_model() + e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) + self.check_span_expression(m, e) + + def test_alternative(self): + m = self.make_model() + e = alternative(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + self.assertIsInstance(e, AlternativeExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "alternative(whole_enchilada, [iv[1], iv[2], iv[3]])") From bda572774a229eec120735966f9facb2647001e6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Jan 2024 16:24:46 -0700 Subject: [PATCH 0257/1178] Making References work for start_time, end_time, length, and is_present --- pyomo/contrib/cp/interval_var.py | 6 +++--- pyomo/contrib/cp/tests/test_interval_var.py | 23 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 2379a58a1ff..fb88ab14832 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -51,7 +51,7 @@ class IntervalVarStartTime(IntervalVarTimePoint): """This class defines a single variable denoting a start time point of an IntervalVar""" - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarStartTime) @@ -59,7 +59,7 @@ class IntervalVarEndTime(IntervalVarTimePoint): """This class defines a single variable denoting an end time point of an IntervalVar""" - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarEndTime) @@ -69,7 +69,7 @@ class IntervalVarLength(ScalarVar): __slots__ = () - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarLength) def get_associated_interval_var(self): diff --git a/pyomo/contrib/cp/tests/test_interval_var.py b/pyomo/contrib/cp/tests/test_interval_var.py index edbf889fcda..1ebb87a67be 100644 --- a/pyomo/contrib/cp/tests/test_interval_var.py +++ b/pyomo/contrib/cp/tests/test_interval_var.py @@ -17,7 +17,7 @@ IntervalVarPresence, ) from pyomo.core.expr import GetItemExpression, GetAttrExpression -from pyomo.environ import ConcreteModel, Integers, Set, value, Var +from pyomo.environ import ConcreteModel, Integers, Reference, Set, value, Var class TestScalarIntervalVar(unittest.TestCase): @@ -217,5 +217,24 @@ def test_index_by_expr(self): self.assertIs(thing2.args[0], thing1) self.assertEqual(thing2.args[1], 'start_time') - # TODO: But this is where it dies. expr1 = m.act[m.i, 2].start_time.before(m.act[m.i**2, 1].end_time) + + def test_reference(self): + m = ConcreteModel() + m.act = IntervalVar([1, 2], end=[0, 10], optional=True) + + thing = Reference(m.act[:].is_present) + self.assertIs(thing[1], m.act[1].is_present) + self.assertIs(thing[2], m.act[2].is_present) + + thing = Reference(m.act[:].start_time) + self.assertIs(thing[1], m.act[1].start_time) + self.assertIs(thing[2], m.act[2].start_time) + + thing = Reference(m.act[:].end_time) + self.assertIs(thing[1], m.act[1].end_time) + self.assertIs(thing[2], m.act[2].end_time) + + thing = Reference(m.act[:].length) + self.assertIs(thing[1], m.act[1].length) + self.assertIs(thing[2], m.act[2].length) From b689a97dcec94fe040c4c7c786ce9f35ba9bee0f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 30 Jan 2024 09:39:16 -0700 Subject: [PATCH 0258/1178] Apply new black --- pyomo/contrib/solver/ipopt.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 534a9173d07..c6c7a6ee17a 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -94,15 +94,15 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.no_function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) - self.timing_info.function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) From 56a36e97ffeb540964014718219051ea306cc9f8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 30 Jan 2024 11:55:12 -0700 Subject: [PATCH 0259/1178] Flesh out unit tests for base classes; change to load_solutions for backwards compatibility --- pyomo/contrib/solver/base.py | 34 ++++--- pyomo/contrib/solver/config.py | 4 +- pyomo/contrib/solver/factory.py | 4 +- pyomo/contrib/solver/ipopt.py | 8 +- .../solver/tests/solvers/test_ipopt.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 90 ++++++++++++++++++- .../contrib/solver/tests/unit/test_config.py | 4 +- 7 files changed, 114 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 0b33f8a5648..e0eb58924c1 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,8 +14,6 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from .config import SolverConfig - from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -29,6 +27,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( Results, @@ -215,7 +214,7 @@ def _get_primals( A map of variables to primals. """ raise NotImplementedError( - '{0} does not support the get_primals method'.format(type(self)) + f'{type(self)} does not support the get_primals method' ) def _get_duals( @@ -235,9 +234,7 @@ def _get_duals( duals: dict Maps constraints to dual values """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) + raise NotImplementedError(f'{type(self)} does not support the get_duals method') def _get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -255,7 +252,7 @@ def _get_reduced_costs( Maps variable to reduced cost """ raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) + f'{type(self)} does not support the get_reduced_costs method' ) @abc.abstractmethod @@ -331,15 +328,20 @@ def update_params(self): """ -class LegacySolverInterface: +class LegacySolverWrapper: """ Class to map the new solver interface features into the legacy solver interface. Necessary for backwards compatibility. """ - def set_config(self, config): - # TODO: Make a mapping from new config -> old config - pass + # + # Support "with" statements + # + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + """Exit statement - enables `with` statements.""" def solve( self, @@ -369,7 +371,7 @@ def solve( original_config = self.config self.config = self.config() self.config.tee = tee - self.config.load_solution = load_solutions + self.config.load_solutions = load_solutions self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit self.config.report_timing = report_timing @@ -489,7 +491,7 @@ def options(self): """ Read the options for the dictated solver. - NOTE: Only the set of solvers for which the LegacySolverInterface is compatible + NOTE: Only the set of solvers for which the LegacySolverWrapper is compatible are accounted for within this property. Not all solvers are currently covered by this backwards compatibility class. @@ -511,9 +513,3 @@ def options(self, val): found = True if not found: raise NotImplementedError('Could not find the correct options') - - def __enter__(self): - return self - - def __exit__(self, t, v, traceback): - pass diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 6068269dcae..4c81d31a820 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -58,8 +58,8 @@ def __init__( description="If True, the solver output gets logged.", ), ) - self.load_solution: bool = self.declare( - 'load_solution', + self.load_solutions: bool = self.declare( + 'load_solutions', ConfigValue( domain=bool, default=True, diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index fa3e2611667..e499605afd4 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -12,7 +12,7 @@ from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory -from pyomo.contrib.solver.base import LegacySolverInterface +from pyomo.contrib.solver.base import LegacySolverWrapper class SolverFactoryClass(Factory): @@ -21,7 +21,7 @@ def decorator(cls): self._cls[name] = cls self._doc[name] = doc - class LegacySolver(LegacySolverInterface, cls): + class LegacySolver(LegacySolverWrapper, cls): pass LegacySolverFactory.register(name, doc)(LegacySolver) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index c6c7a6ee17a..7c2a3f471e3 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -390,15 +390,15 @@ def solve(self, model, **kwds): results.solver_name = 'ipopt' results.solver_version = self.version() if ( - config.load_solution + config.load_solutions and results.solution_status == SolutionStatus.noSolution ): raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' - 'Please set config.load_solution=False to bypass this error.' + 'Please set config.load_solutions=False to bypass this error.' ) - if config.load_solution: + if config.load_solutions: results.solution_loader.load_vars() if ( hasattr(model, 'dual') @@ -417,7 +417,7 @@ def solve(self, model, **kwds): results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal} and len(nl_info.objectives) > 0 ): - if config.load_solution: + if config.load_solutions: results.incumbent_objective = value(nl_info.objectives[0]) else: results.incumbent_objective = value( diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index 9638d94bdda..627d502629c 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -43,7 +43,7 @@ def rosenbrock(m): def test_ipopt_config(self): # Test default initialization config = ipoptConfig() - self.assertTrue(config.load_solution) + self.assertTrue(config.load_solutions) self.assertIsInstance(config.solver_options, ConfigDict) self.assertIsInstance(config.executable, ExecutableData) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index e3a8999d8c5..dd94ef18fc3 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -14,8 +14,27 @@ class TestSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['solve', 'available', 'version'] + member_list = list(base.SolverBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + def test_class_method_list(self): + expected_list = [ + 'Availability', + 'CONFIG', + 'available', + 'is_persistent', + 'solve', + 'version', + ] + method_list = [ + method for method in dir(base.SolverBase) if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) - def test_solver_base(self): + def test_init(self): self.instance = base.SolverBase() self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) @@ -23,6 +42,20 @@ def test_solver_base(self): self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_context_manager(self): + with base.SolverBase() as self.instance: + self.assertFalse(self.instance.is_persistent()) + self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.CONFIG, self.instance.config) + self.assertEqual(self.instance.solve(None), None) + self.assertEqual(self.instance.available(), None) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_config_kwds(self): + self.instance = base.SolverBase(tee=True) + self.assertTrue(self.instance.config.tee) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) def test_solver_availability(self): self.instance = base.SolverBase() @@ -57,8 +90,41 @@ def test_abstract_member_list(self): member_list = list(base.PersistentSolverBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) + def test_class_method_list(self): + expected_list = [ + 'Availability', + 'CONFIG', + '_abc_impl', + '_get_duals', + '_get_primals', + '_get_reduced_costs', + '_load_vars', + 'add_block', + 'add_constraints', + 'add_params', + 'add_variables', + 'available', + 'is_persistent', + 'remove_block', + 'remove_constraints', + 'remove_params', + 'remove_variables', + 'set_instance', + 'set_objective', + 'solve', + 'update_params', + 'update_variables', + 'version', + ] + method_list = [ + method + for method in dir(base.PersistentSolverBase) + if method.startswith('__') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) - def test_persistent_solver_base(self): + def test_init(self): self.instance = base.PersistentSolverBase() self.assertTrue(self.instance.is_persistent()) self.assertEqual(self.instance.set_instance(None), None) @@ -82,3 +148,23 @@ def test_persistent_solver_base(self): with self.assertRaises(NotImplementedError): self.instance._get_reduced_costs() + + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) + def test_context_manager(self): + with base.PersistentSolverBase() as self.instance: + self.assertTrue(self.instance.is_persistent()) + self.assertEqual(self.instance.set_instance(None), None) + self.assertEqual(self.instance.add_variables(None), None) + self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_constraints(None), None) + self.assertEqual(self.instance.add_block(None), None) + self.assertEqual(self.instance.remove_variables(None), None) + self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_constraints(None), None) + self.assertEqual(self.instance.remove_block(None), None) + self.assertEqual(self.instance.set_objective(None), None) + self.assertEqual(self.instance.update_variables(None), None) + self.assertEqual(self.instance.update_params(), None) + +class TestLegacySolverWrapper(unittest.TestCase): + pass diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index 3ad8319343b..4a7cc250623 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -19,7 +19,7 @@ def test_interface_default_instantiation(self): self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) - self.assertTrue(config.load_solution) + self.assertTrue(config.load_solutions) self.assertTrue(config.raise_exception_on_nonoptimal_result) self.assertFalse(config.symbolic_solver_labels) self.assertIsNone(config.timer) @@ -43,7 +43,7 @@ def test_interface_default_instantiation(self): self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) - self.assertTrue(config.load_solution) + self.assertTrue(config.load_solutions) self.assertFalse(config.symbolic_solver_labels) self.assertIsNone(config.rel_gap) self.assertIsNone(config.abs_gap) From ed87c162cdb0239171a681344acc2d66b706b0dd Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 30 Jan 2024 12:06:28 -0700 Subject: [PATCH 0260/1178] Fixed parmest tests of new interface. --- .../parmest/deprecated/tests/test_examples.py | 34 +- .../parmest/deprecated/tests/test_parmest.py | 6 +- .../deprecated/tests/test_scenariocreator.py | 4 +- .../parmest/deprecated/tests/test_utils.py | 2 +- .../examples/reactor_design/reactor_design.py | 5 +- .../examples/rooney_biegler/rooney_biegler.py | 1 - .../semibatch/parameter_estimation_example.py | 20 +- .../examples/semibatch/scenario_example.py | 17 +- .../parmest/examples/semibatch/semibatch.py | 32 ++ pyomo/contrib/parmest/graphics.py | 2 +- pyomo/contrib/parmest/parmest.py | 31 +- pyomo/contrib/parmest/scenariocreator.py | 4 +- pyomo/contrib/parmest/tests/test_parmest.py | 348 +++++++++++------- .../parmest/tests/test_scenariocreator.py | 30 +- pyomo/contrib/parmest/tests/test_utils.py | 9 +- 15 files changed, 347 insertions(+), 198 deletions(-) diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py index 67e06130384..04aff572529 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_examples.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_examples.py @@ -32,12 +32,12 @@ def tearDownClass(self): pass def test_model(self): - from pyomo.contrib.parmest.examples.rooney_biegler import rooney_biegler + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import rooney_biegler rooney_biegler.main() def test_model_with_constraint(self): - from pyomo.contrib.parmest.examples.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( rooney_biegler_with_constraint, ) @@ -45,7 +45,7 @@ def test_model_with_constraint(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.examples.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( parameter_estimation_example, ) @@ -53,13 +53,13 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.examples.rooney_biegler import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import bootstrap_example bootstrap_example.main() @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.examples.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( likelihood_ratio_example, ) @@ -81,7 +81,7 @@ def tearDownClass(self): pass def test_example(self): - from pyomo.contrib.parmest.examples.reaction_kinetics import ( + from pyomo.contrib.parmest.deprecated.examples.reaction_kinetics import ( simple_reaction_parmest_example, ) @@ -103,19 +103,19 @@ def tearDownClass(self): pass def test_model(self): - from pyomo.contrib.parmest.examples.semibatch import semibatch + from pyomo.contrib.parmest.deprecated.examples.semibatch import semibatch semibatch.main() def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.examples.semibatch import ( + from pyomo.contrib.parmest.deprecated.examples.semibatch import ( parameter_estimation_example, ) parameter_estimation_example.main() def test_scenario_example(self): - from pyomo.contrib.parmest.examples.semibatch import scenario_example + from pyomo.contrib.parmest.deprecated.examples.semibatch import scenario_example scenario_example.main() @@ -136,12 +136,12 @@ def tearDownClass(self): @unittest.pytest.mark.expensive def test_model(self): - from pyomo.contrib.parmest.examples.reactor_design import reactor_design + from pyomo.contrib.parmest.deprecated.examples.reactor_design import reactor_design reactor_design.main() def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( parameter_estimation_example, ) @@ -149,13 +149,13 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.examples.reactor_design import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import bootstrap_example bootstrap_example.main() @unittest.pytest.mark.expensive def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( likelihood_ratio_example, ) @@ -163,19 +163,19 @@ def test_likelihood_ratio_example(self): @unittest.pytest.mark.expensive def test_leaveNout_example(self): - from pyomo.contrib.parmest.examples.reactor_design import leaveNout_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import leaveNout_example leaveNout_example.main() def test_timeseries_data_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( timeseries_data_example, ) timeseries_data_example.main() def test_multisensor_data_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( multisensor_data_example, ) @@ -183,7 +183,7 @@ def test_multisensor_data_example(self): @unittest.skipUnless(matplotlib_available, "test requires matplotlib") def test_datarec_example(self): - from pyomo.contrib.parmest.examples.reactor_design import datarec_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import datarec_example datarec_example.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py index 7e692989b0c..40c98dac3af 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py @@ -54,7 +54,7 @@ @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestRooneyBiegler(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler import ( rooney_biegler_model, ) @@ -605,7 +605,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestReactorDesign(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) @@ -879,7 +879,7 @@ def test_covariance(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestSquareInitialization_RooneyBiegler(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler_with_constraint import ( rooney_biegler_model_with_constraint, ) diff --git a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py index 22a851ae32e..54cbe80f73c 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py @@ -36,7 +36,7 @@ @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestScenarioReactorDesign(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) @@ -112,7 +112,7 @@ def test_no_csv_if_empty(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestScenarioSemibatch(unittest.TestCase): def setUp(self): - import pyomo.contrib.parmest.examples.semibatch.semibatch as sb + import pyomo.contrib.parmest.deprecated.examples.semibatch.semibatch as sb import json # Vars to estimate in parmest diff --git a/pyomo/contrib/parmest/deprecated/tests/test_utils.py b/pyomo/contrib/parmest/deprecated/tests/test_utils.py index 514c14b1e82..1a8247ddcc9 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_utils.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_utils.py @@ -35,7 +35,7 @@ def tearDownClass(self): @unittest.pytest.mark.expensive def test_convert_param_to_var(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 1479009abcc..db3b0e1d380 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -118,7 +118,7 @@ def get_labeled_model(self): return m -if __name__ == "__main__": +def main(): # For a range of sv values, return ca, cb, cc, and cd results = [] @@ -142,4 +142,7 @@ def get_labeled_model(self): results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 2ac03504260..6e7d6219a64 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -62,7 +62,6 @@ def label_model(self): m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) - m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant]) diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index fc4c9f5c675..145569f7535 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -12,12 +12,11 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model - +from pyomo.contrib.parmest.examples.semibatch.semibatch import ( + SemiBatchExperiment, +) def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] # Data, list of dictionaries data = [] @@ -28,11 +27,20 @@ def main(): d = json.load(infile) data.append(d) + # Create an experiment list + exp_list= [] + for i in range(len(data)): + exp_list.append(SemiBatchExperiment(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Note, the model already includes a 'SecondStageCost' expression # for sum of squared error that will be used in parameter estimation - pest = parmest.Estimator(generate_model, data, theta_names) - + pest = parmest.Estimator(exp_list) + obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index 071e53236c4..a80a82671bc 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -12,13 +12,13 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model +from pyomo.contrib.parmest.examples.semibatch.semibatch import ( + SemiBatchExperiment, +) import pyomo.contrib.parmest.scenariocreator as sc def main(): - # Vars to estimate in parmest - theta_names = ['k1', 'k2', 'E1', 'E2'] # Data: list of dictionaries data = [] @@ -29,7 +29,16 @@ def main(): d = json.load(infile) data.append(d) - pest = parmest.Estimator(generate_model, data, theta_names) + # Create an experiment list + exp_list= [] + for i in range(len(data)): + exp_list.append(SemiBatchExperiment(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list) scenmaker = sc.ScenarioCreator(pest, "ipopt") diff --git a/pyomo/contrib/parmest/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/examples/semibatch/semibatch.py index 6762531a338..3ef7bc01aa9 100644 --- a/pyomo/contrib/parmest/examples/semibatch/semibatch.py +++ b/pyomo/contrib/parmest/examples/semibatch/semibatch.py @@ -29,8 +29,11 @@ SolverFactory, exp, minimize, + Suffix, + ComponentUID, ) from pyomo.dae import ContinuousSet, DerivativeVar +from pyomo.contrib.parmest.experiment import Experiment def generate_model(data): @@ -268,6 +271,35 @@ def total_cost_rule(model): return m +class SemiBatchExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = generate_model(self.data) + + def label_model(self): + + m = self.model + + m.unknown_parameters = Suffix(direction=Suffix.LOCAL) + m.unknown_parameters.update((k, ComponentUID(k)) + for k in [m.k1, m.k2, m.E1, m.E2]) + + + def finalize_model(self): + pass + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # Data loaded from files file_dirname = dirname(abspath(str(__file__))) diff --git a/pyomo/contrib/parmest/graphics.py b/pyomo/contrib/parmest/graphics.py index b8dfa243b9a..65efb5cfd64 100644 --- a/pyomo/contrib/parmest/graphics.py +++ b/pyomo/contrib/parmest/graphics.py @@ -152,7 +152,7 @@ def _add_scipy_dist_CI( data_slice.append(np.array([[theta_star[var]] * ncells] * ncells)) data_slice = np.dstack(tuple(data_slice)) - elif isinstance(dist, stats.kde.gaussian_kde): + elif isinstance(dist, stats.gaussian_kde): for var in theta_star.index: if var == xvar: data_slice.append(X.ravel()) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a00671c2ea6..e256b0f38d7 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -745,7 +745,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): for snum in scenario_numbers: sname = "scenario_NODE" + str(snum) instance = _experiment_instance_creation_callback(sname, None, dummy_cb) - model_theta_names = [k.name for k,v in instance.unknown_parameters.items()] + model_theta_names = self._expand_indexed_unknowns(instance) if initialize_parmest_model: # list to store fitted parameter names that will be unfixed @@ -1186,6 +1186,28 @@ def leaveNout_bootstrap_test( return results + # expand indexed variables to get full list of thetas + def _expand_indexed_unknowns(self, model_temp): + + model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] + + # check for indexed theta items + indexed_theta_list = [] + for theta_i in model_theta_list: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + for ind in var_validate.index_set(): + if ind is not None: + indexed_theta_list.append(theta_i + '[' + str(ind) + ']') + else: + indexed_theta_list.append(theta_i) + + # if we found indexed thetas, use expanded list + if len(indexed_theta_list) > len(model_theta_list): + model_theta_list = indexed_theta_list + + return model_theta_list + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ Objective value for each theta @@ -1218,8 +1240,8 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): else: # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) - model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] - + model_theta_list = self._expand_indexed_unknowns(model_temp) + # # iterate over original theta_names # for theta_i in self.theta_names: # var_cuid = ComponentUID(theta_i) @@ -1254,7 +1276,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member - theta_names = self.estimator_theta_names() + theta_names = model_theta_list else: assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop @@ -1262,7 +1284,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # # check if theta_names are in model for theta in list(theta_names): theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - assert theta_temp in [ t.replace("'", "") for t in model_theta_list ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index b849bfdfd5b..c48ac2bf027 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -151,13 +151,13 @@ def ScenariosFromExperiments(self, addtoSet): assert isinstance(addtoSet, ScenarioSet) - scenario_numbers = list(range(len(self.pest.callback_data))) + scenario_numbers = list(range(len(self.pest.exp_list))) prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data + exp_num, ) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 2cc8ad36b0a..b88871e0dbc 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -33,6 +33,7 @@ import pyomo.contrib.parmest.parmest as parmest import pyomo.contrib.parmest.graphics as graphics import pyomo.contrib.parmest as parmestbase +from pyomo.contrib.parmest.experiment import Experiment import pyomo.environ as pyo import pyomo.dae as dae @@ -46,7 +47,6 @@ testdir = os.path.dirname(os.path.abspath(__file__)) - @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -55,7 +55,7 @@ class TestRooneyBiegler(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) @@ -64,23 +64,26 @@ def setUp(self): columns=["hour", "y"], ) - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) + # Sum of squared error function + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # Create an instance of the parmest estimator + pest = parmest.Estimator(exp_list, obj_function=SSE) + solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( - rooney_biegler_model, - data, - theta_names, - SSE, + exp_list, + obj_function=SSE, solver_options=solver_options, tee=True, ) @@ -229,15 +232,17 @@ def test_theta_est_cov(self): # Covariance matrix self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 + cov['asymptote']['asymptote'], 6.30579403, places=2 ) # 6.22864 from paper self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 + cov['asymptote']['rate_constant'], -0.4395341, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 + cov['rate_constant']['asymptote'], -0.4395341, places=2 ) # -0.4322 from paper - self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper + self.assertAlmostEqual( + cov['rate_constant']['rate_constant'], 0.04124, places=2 + ) # 0.04124 from paper """ Why does the covariance matrix from parmest not match the paper? Parmest is calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely @@ -348,7 +353,12 @@ def model(t, asymptote, rate_constant): ) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestModelVariants(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + RooneyBieglerExperiment, + ) + self.data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], @@ -360,6 +370,9 @@ def rooney_biegler_params(data): model.asymptote = pyo.Param(initialize=15, mutable=True) model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -368,6 +381,17 @@ def response_rule(m, h): return model + class RooneyBieglerExperimentParams(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_params(self.data) + + rooney_biegler_params_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_params_exp_list.append( + RooneyBieglerExperimentParams(self.data.loc[i,:].to_frame().transpose()) + ) + def rooney_biegler_indexed_params(data): model = pyo.ConcreteModel() @@ -378,6 +402,9 @@ def rooney_biegler_indexed_params(data): mutable=True, ) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.theta["asymptote"] * ( 1 - pyo.exp(-m.theta["rate_constant"] * h) @@ -388,6 +415,29 @@ def response_rule(m, h): return model + class RooneyBieglerExperimentIndexedParams(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_indexed_params(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.theta]) + + rooney_biegler_indexed_params_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_indexed_params_exp_list.append( + RooneyBieglerExperimentIndexedParams(self.data.loc[i,:].to_frame().transpose()) + ) + def rooney_biegler_vars(data): model = pyo.ConcreteModel() @@ -396,6 +446,9 @@ def rooney_biegler_vars(data): model.asymptote.fixed = True # parmest will unfix theta variables model.rate_constant.fixed = True + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -404,6 +457,17 @@ def response_rule(m, h): return model + class RooneyBieglerExperimentVars(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_vars(self.data) + + rooney_biegler_vars_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_vars_exp_list.append( + RooneyBieglerExperimentVars(self.data.loc[i,:].to_frame().transpose()) + ) + def rooney_biegler_indexed_vars(data): model = pyo.ConcreteModel() @@ -418,6 +482,9 @@ def rooney_biegler_indexed_vars(data): ) model.theta["rate_constant"].fixed = True + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.theta["asymptote"] * ( 1 - pyo.exp(-m.theta["rate_constant"] * h) @@ -428,11 +495,34 @@ def response_rule(m, h): return model - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index + class RooneyBieglerExperimentIndexedVars(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_indexed_vars(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.theta]) + + + rooney_biegler_indexed_vars_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_indexed_vars_exp_list.append( + RooneyBieglerExperimentIndexedVars(self.data.loc[i,:].to_frame().transpose()) ) + + # Sum of squared error function + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr self.objective_function = SSE @@ -444,32 +534,32 @@ def SSE(model, data): self.input = { "param": { - "model": rooney_biegler_params, + "exp_list": rooney_biegler_params_exp_list, "theta_names": ["asymptote", "rate_constant"], "theta_vals": theta_vals, }, "param_index": { - "model": rooney_biegler_indexed_params, + "exp_list": rooney_biegler_indexed_params_exp_list, "theta_names": ["theta"], "theta_vals": theta_vals_index, }, "vars": { - "model": rooney_biegler_vars, + "exp_list": rooney_biegler_vars_exp_list, "theta_names": ["asymptote", "rate_constant"], "theta_vals": theta_vals, }, "vars_index": { - "model": rooney_biegler_indexed_vars, + "exp_list": rooney_biegler_indexed_vars_exp_list, "theta_names": ["theta"], "theta_vals": theta_vals_index, }, "vars_quoted_index": { - "model": rooney_biegler_indexed_vars, + "exp_list": rooney_biegler_indexed_vars_exp_list, "theta_names": ["theta['asymptote']", "theta['rate_constant']"], "theta_vals": theta_vals_index, }, "vars_str_index": { - "model": rooney_biegler_indexed_vars, + "exp_list": rooney_biegler_indexed_vars_exp_list, "theta_names": ["theta[asymptote]", "theta[rate_constant]"], "theta_vals": theta_vals_index, }, @@ -480,58 +570,52 @@ def SSE(model, data): not parmest.inverse_reduced_hessian_available, "Cannot test covariance matrix: required ASL dependency is missing", ) + + def check_rooney_biegler_results(self, objval, cov): + + # get indices in covariance matrix + cov_cols = cov.columns.to_list() + asymptote_index = [idx for idx, s in enumerate(cov_cols) if 'asymptote' in s][0] + rate_constant_index = [idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s][0] + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[asymptote_index, asymptote_index], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[asymptote_index, rate_constant_index], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[rate_constant_index, asymptote_index], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[rate_constant_index, rate_constant_index], 0.04193591, places=2 + ) # 0.04124 from paper + def test_parmest_basics(self): + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper + self.check_rooney_biegler_results(objval, cov) obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_initialize_parmest_model_option(self): - for model_type, parmest_input in self.input.items(): + + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper + self.check_rooney_biegler_results(objval, cov) obj_at_theta = pest.objective_at_theta( parmest_input["theta_vals"], initialize_parmest_model=True @@ -540,12 +624,11 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_square_problem_solve(self): + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) obj_at_theta = pest.objective_at_theta( @@ -553,50 +636,23 @@ def test_parmest_basics_with_square_problem_solve(self): ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper + self.check_rooney_biegler_results(objval, cov) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - + self.check_rooney_biegler_results(objval, cov) @unittest.skipIf( not parmest.parmest_available, @@ -606,7 +662,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): class TestReactorDesign(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) # Data from the design @@ -635,22 +691,14 @@ def setUp(self): columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) solver_options = {"max_iter": 6000} - self.pest = parmest.Estimator( - reactor_design_model, data, theta_names, SSE, solver_options=solver_options - ) + self.pest = parmest.Estimator(exp_list, obj_function='SSE', solver_options=solver_options) def test_theta_est(self): # used in data reconciliation @@ -759,6 +807,30 @@ def total_cost_rule(model): return m + class ReactorDesignExperimentDAE(Experiment): + + def __init__(self, data): + + self.data = data + self.model = None + + def create_model(self): + self.model = ABC_model(self.data) + + def label_model(self): + + m = self.model + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k1, m.k2]) + + def get_labeled_model(self): + self.create_model() + self.label_model() + + return self.model + # This example tests data formatted in 3 ways # Each format holds 1 scenario # 1. dataframe with time index @@ -793,18 +865,21 @@ def total_cost_rule(model): "cc": {k: v for (k, v) in zip(data.t, data.cc)}, } - theta_names = ["k1", "k2"] + # Create an experiment list + exp_list_df = [ReactorDesignExperimentDAE(data_df)] + exp_list_dict = [ReactorDesignExperimentDAE(data_dict)] - self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) - self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) + self.pest_df = parmest.Estimator(exp_list_df) + self.pest_dict = parmest.Estimator(exp_list_dict) # Estimator object with multiple scenarios - self.pest_df_multiple = parmest.Estimator( - ABC_model, [data_df, data_df], theta_names - ) - self.pest_dict_multiple = parmest.Estimator( - ABC_model, [data_dict, data_dict], theta_names - ) + exp_list_df_multiple = [ReactorDesignExperimentDAE(data_df), + ReactorDesignExperimentDAE(data_df)] + exp_list_dict_multiple = [ReactorDesignExperimentDAE(data_dict), + ReactorDesignExperimentDAE(data_dict)] + + self.pest_df_multiple = parmest.Estimator(exp_list_df_multiple) + self.pest_dict_multiple = parmest.Estimator(exp_list_dict_multiple) # Create an instance of the model self.m_df = ABC_model(data_df) @@ -880,7 +955,7 @@ def test_covariance(self): class TestSquareInitialization_RooneyBiegler(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( - rooney_biegler_model_with_constraint, + RooneyBieglerExperiment, ) # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) @@ -888,24 +963,25 @@ def setUp(self): data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], ) + + # Sum of squared error function + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 + return expr - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index + exp_list = [] + for i in range(data.shape[0]): + exp_list.append( + RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose()) ) - return expr solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( - rooney_biegler_model_with_constraint, - data, - theta_names, - SSE, + exp_list, + obj_function=SSE, solver_options=solver_options, tee=True, ) diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index 22a851ae32e..bf6fa12b8b1 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -37,7 +37,7 @@ class TestScenarioReactorDesign(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) # Data from the design @@ -65,19 +65,13 @@ def setUp(self): ], columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + self.pest = parmest.Estimator(exp_list, obj_function='SSE') def test_scen_from_exps(self): scenmaker = sc.ScenarioCreator(self.pest, "ipopt") @@ -115,9 +109,6 @@ def setUp(self): import pyomo.contrib.parmest.examples.semibatch.semibatch as sb import json - # Vars to estimate in parmest - theta_names = ["k1", "k2", "E1", "E2"] - self.fbase = os.path.join(testdir, "..", "examples", "semibatch") # Data, list of dictionaries data = [] @@ -131,7 +122,12 @@ def setUp(self): # Note, the model already includes a 'SecondStageCost' expression # for the sum of squared error that will be used in parameter estimation - self.pest = parmest.Estimator(sb.generate_model, data, theta_names) + # Create an experiment list + exp_list= [] + for i in range(len(data)): + exp_list.append(sb.SemiBatchExperiment(data[i])) + + self.pest = parmest.Estimator(exp_list) def test_semibatch_bootstrap(self): scenmaker = sc.ScenarioCreator(self.pest, "ipopt") diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 514c14b1e82..99ba7b7cd90 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -48,12 +48,17 @@ def test_convert_param_to_var(self): columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ["k1", "k2", "k3"] + # make model + instance = reactor_design_model() + + # add caf, sv + instance.caf = data.iloc[0]['caf'] + instance.sv = data.iloc[0]['sv'] - instance = reactor_design_model(data.loc[0]) solver = pyo.SolverFactory("ipopt") solver.solve(instance) + theta_names = ['k1', 'k2', 'k3'] instance_vars = parmest.utils.convert_params_to_vars( instance, theta_names, fix_vars=True ) From 9d7e5c0b15e4758e3fe318995a990b04205cd226 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 30 Jan 2024 12:29:52 -0700 Subject: [PATCH 0261/1178] Save state: making Legacy wrapper more testable --- pyomo/contrib/solver/base.py | 103 +++++++++++++------ pyomo/contrib/solver/tests/unit/test_base.py | 1 + 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index e0eb58924c1..98fa60b722f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -343,32 +343,21 @@ def __enter__(self): def __exit__(self, t, v, traceback): """Exit statement - enables `with` statements.""" - def solve( + def _map_config( self, - model: _BlockData, - tee: bool = False, - load_solutions: bool = True, - logfile: Optional[str] = None, - solnfile: Optional[str] = None, - timelimit: Optional[float] = None, - report_timing: bool = False, - solver_io: Optional[str] = None, - suffixes: Optional[Sequence] = None, - options: Optional[Dict] = None, - keepfiles: bool = False, - symbolic_solver_labels: bool = False, - raise_exception_on_nonoptimal_result: bool = False, + tee, + load_solutions, + symbolic_solver_labels, + timelimit, + report_timing, + raise_exception_on_nonoptimal_result, + solver_io, + suffixes, + logfile, + keepfiles, + solnfile, ): - """ - Solve method: maps new solve method style to backwards compatible version. - - Returns - ------- - legacy_results - Legacy results object - - """ - original_config = self.config + """Map between legacy and new interface configuration options""" self.config = self.config() self.config.tee = tee self.config.load_solutions = load_solutions @@ -392,12 +381,9 @@ def solve( if 'filename' in self.config: filename = os.path.splitext(solnfile)[0] self.config.filename = filename - original_options = self.options - if options is not None: - self.options = options - - results: Results = super().solve(model) + def _map_results(self, model, results): + """Map between legacy and new Results objects""" legacy_results = LegacySolverResults() legacy_soln = LegacySolution() legacy_results.solver.status = legacy_solver_status_map[ @@ -408,7 +394,6 @@ def solve( ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - obj = get_objective(model) if len(list(obj)) > 0: legacy_results.problem.sense = obj.sense @@ -426,12 +411,16 @@ def solve( legacy_soln.gap = abs(results.incumbent_objective - results.objective_bound) else: legacy_soln.gap = None + return legacy_results, legacy_soln + def _solution_handler( + self, load_solutions, model, results, legacy_results, legacy_soln + ): + """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) - delete_legacy_soln = True if load_solutions: if hasattr(model, 'dual') and model.dual.import_enabled(): @@ -454,6 +443,58 @@ def solve( legacy_results.solution.insert(legacy_soln) if delete_legacy_soln: legacy_results.solution.delete(0) + return legacy_results + + def solve( + self, + model: _BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + raise_exception_on_nonoptimal_result: bool = False, + ): + """ + Solve method: maps new solve method style to backwards compatible version. + + Returns + ------- + legacy_results + Legacy results object + + """ + original_config = self.config + self._map_config( + tee, + load_solutions, + symbolic_solver_labels, + timelimit, + report_timing, + raise_exception_on_nonoptimal_result, + solver_io, + suffixes, + logfile, + keepfiles, + solnfile, + ) + + original_options = self.options + if options is not None: + self.options = options + + results: Results = super().solve(model) + legacy_results, legacy_soln = self._map_results(model, results) + + legacy_results = self._solution_handler( + load_solutions, model, results, legacy_results, legacy_soln + ) self.config = original_config self.options = original_options diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index dd94ef18fc3..2d158025903 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -166,5 +166,6 @@ def test_context_manager(self): self.assertEqual(self.instance.update_variables(None), None) self.assertEqual(self.instance.update_params(), None) + class TestLegacySolverWrapper(unittest.TestCase): pass From 8f7f95b2bcacb5e04910a81fe46643f9d5329162 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Jan 2024 15:49:41 -0700 Subject: [PATCH 0262/1178] Adding tests for the pyomo-to-docplex map on the walker --- pyomo/contrib/cp/tests/test_docplex_walker.py | 224 ++++++++++++++++-- 1 file changed, 210 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index fc475190ade..73b4fd8e00c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -107,6 +107,10 @@ def test_write_addition(self): expr[1].equals(cpx_x + cp.start_of(cpx_i) + cp.length_of(cpx_i2)) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], cpx_x) + self.assertIs(visitor.pyomo_to_docplex[m.i], cpx_i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], cpx_i2) + def test_write_subtraction(self): m = self.get_model() m.a.domain = Binary @@ -122,6 +126,9 @@ def test_write_subtraction(self): self.assertTrue(expr[1].equals(x + (-1 * a1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_product(self): m = self.get_model() m.a.domain = PositiveIntegers @@ -137,6 +144,9 @@ def test_write_product(self): self.assertTrue(expr[1].equals(x * (a1 + 1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_floating_point_division(self): m = self.get_model() m.a.domain = NonNegativeIntegers @@ -152,6 +162,9 @@ def test_write_floating_point_division(self): self.assertTrue(expr[1].equals(x / (a1 + 1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_power_expression(self): m = self.get_model() m.c = Constraint(expr=m.x**2 <= 3) @@ -163,6 +176,8 @@ def test_write_power_expression(self): # .equals checks the equality of two expressions in docplex. self.assertTrue(expr[1].equals(cpx_x**2)) + self.assertIs(visitor.pyomo_to_docplex[m.x], cpx_x) + def test_write_absolute_value_expression(self): m = self.get_model() m.a.domain = NegativeIntegers @@ -176,6 +191,8 @@ def test_write_absolute_value_expression(self): self.assertTrue(expr[1].equals(cp.abs(a1) + 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_min_expression(self): m = self.get_model() m.a.domain = NonPositiveIntegers @@ -187,6 +204,7 @@ def test_write_min_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.min(a[i] for i in m.I))) @@ -201,6 +219,7 @@ def test_write_max_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.max(a[i] for i in m.I))) @@ -235,6 +254,14 @@ def test_write_logical_and(self): self.assertTrue(expr[1].equals(cp.logical_and(b, b2b))) + # ESJ: This is ludicrous, but I don't know how to get the args of a CP + # expression, so testing that we were correct in the pyomo to docplex + # map by checking that we can build an expression that is the same as b + # (because b is actually "b == 1" since docplex doesn't believe in + # Booleans) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertTrue(b2b.equals(visitor.pyomo_to_docplex[m.b2['b']] == 1)) + def test_write_logical_or(self): m = self.get_model() m.c = LogicalConstraint(expr=m.b.lor(m.i.is_present)) @@ -248,6 +275,9 @@ def test_write_logical_or(self): self.assertTrue(expr[1].equals(cp.logical_or(b, cp.presence_of(i)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + def test_write_xor(self): m = self.get_model() m.c = LogicalConstraint(expr=m.b.xor(m.i2[2].start_time >= 5)) @@ -265,6 +295,9 @@ def test_write_xor(self): expr[1].equals(cp.count([b, cp.less_or_equal(5, cp.start_of(i22))], 1) == 1) ) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_write_logical_not(self): m = self.get_model() m.c = LogicalConstraint(expr=~m.b2['a']) @@ -276,6 +309,8 @@ def test_write_logical_not(self): self.assertTrue(expr[1].equals(cp.logical_not(b2a))) + self.assertTrue(b2a.equals(visitor.pyomo_to_docplex[m.b2['a']] == 1)) + def test_equivalence(self): m = self.get_model() m.c = LogicalConstraint(expr=equivalent(~m.b2['a'], m.b)) @@ -289,18 +324,8 @@ def test_equivalence(self): self.assertTrue(expr[1].equals(cp.equal(cp.logical_not(b2a), b))) - def test_implication(self): - m = self.get_model() - m.c = LogicalConstraint(expr=m.b2['a'].implies(~m.b)) - visitor = self.get_visitor() - expr = visitor.walk_expression((m.c.expr, m.c, 0)) - - self.assertIn(id(m.b), visitor.var_map) - self.assertIn(id(m.b2['a']), visitor.var_map) - b = visitor.var_map[id(m.b)] - b2a = visitor.var_map[id(m.b2['a'])] - - self.assertTrue(expr[1].equals(cp.if_then(b2a, cp.logical_not(b)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertTrue(b2a.equals(visitor.pyomo_to_docplex[m.b2['a']] == 1)) def test_equality(self): m = self.get_model() @@ -317,6 +342,9 @@ def test_equality(self): self.assertTrue(expr[1].equals(cp.if_then(b, cp.equal(a3, 4)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + def test_inequality(self): m = self.get_model() m.a.domain = Integers @@ -334,6 +362,10 @@ def test_inequality(self): self.assertTrue(expr[1].equals(cp.if_then(b, cp.less_or_equal(a4, a3)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + self.assertIs(visitor.pyomo_to_docplex[m.a[4]], a4) + def test_ranged_inequality(self): m = self.get_model() m.a.domain = Integers @@ -364,6 +396,10 @@ def test_not_equal(self): self.assertTrue(expr[1].equals(cp.if_then(b, a3 != a4))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + self.assertIs(visitor.pyomo_to_docplex[m.a[4]], a4) + def test_exactly_expression(self): m = self.get_model() m.a.domain = Integers @@ -376,6 +412,7 @@ def test_exactly_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals(cp.equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) @@ -393,6 +430,7 @@ def test_atleast_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals( @@ -412,6 +450,7 @@ def test_atmost_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals(cp.less_or_equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) @@ -430,6 +469,7 @@ def test_all_diff_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) @@ -449,6 +489,9 @@ def test_Boolean_args_in_all_diff_expression(self): self.assertTrue(expr[1].equals(cp.all_diff(a0 == 13, b))) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a0) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_count_if_expression(self): m = self.get_model() m.a.domain = Integers @@ -462,6 +505,7 @@ def test_count_if_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.count((a[i] == i for i in m.I), 1) == 5)) @@ -480,6 +524,9 @@ def test_interval_var_is_present(self): self.assertTrue(expr[1].equals(cp.if_then(cp.presence_of(i), a1 == 5))) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + def test_interval_var_is_present_indirection(self): m = self.get_model() m.a.domain = Integers @@ -513,6 +560,11 @@ def test_interval_var_is_present_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_is_present_indirection_and_length(self): m = self.get_model() m.y = Var(domain=Integers, bounds=[1, 2]) @@ -547,6 +599,10 @@ def test_is_present_indirection_and_length(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_handle_getattr_lor(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -578,6 +634,11 @@ def test_handle_getattr_lor(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_handle_getattr_xor(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -616,6 +677,11 @@ def test_handle_getattr_xor(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_handle_getattr_equivalent_to(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -647,6 +713,11 @@ def test_handle_getattr_equivalent_to(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_logical_or_on_indirection(self): m = ConcreteModel() m.b = BooleanVar([2, 3, 4, 5]) @@ -676,6 +747,11 @@ def test_logical_or_on_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertTrue(b3.equals(visitor.pyomo_to_docplex[m.b[3]] == 1)) + self.assertTrue(b4.equals(visitor.pyomo_to_docplex[m.b[4]] == 1)) + self.assertTrue(b5.equals(visitor.pyomo_to_docplex[m.b[5]] == 1)) + def test_logical_xor_on_indirection(self): m = ConcreteModel() m.b = BooleanVar([2, 3, 4, 5]) @@ -710,6 +786,10 @@ def test_logical_xor_on_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertTrue(b3.equals(visitor.pyomo_to_docplex[m.b[3]] == 1)) + self.assertTrue(b5.equals(visitor.pyomo_to_docplex[m.b[5]] == 1)) + def test_using_precedence_expr_as_boolean_expr(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.before(m.i2[1].start_time)) @@ -729,6 +809,10 @@ def test_using_precedence_expr_as_boolean_expr(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + 0 <= cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_using_precedence_expr_as_boolean_expr_positive_delay(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.before(m.i2[1].start_time, delay=4)) @@ -748,6 +832,10 @@ def test_using_precedence_expr_as_boolean_expr_positive_delay(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + 4 <= cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_using_precedence_expr_as_boolean_expr_negative_delay(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.at(m.i2[1].start_time, delay=-3)) @@ -767,6 +855,10 @@ def test_using_precedence_expr_as_boolean_expr_negative_delay(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + (-3) == cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_IntervalVars(CommonTest): @@ -780,6 +872,7 @@ def test_interval_var_fixed_presences_correct(self): i = visitor.var_map[id(m.i)] # Check that docplex knows it's optional self.assertTrue(i.is_optional()) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) # Now fix it to absent m.i.is_present.fix(False) @@ -790,8 +883,10 @@ def test_interval_var_fixed_presences_correct(self): self.assertIn(id(m.i2[1]), visitor.var_map) i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) # Check that we passed on the presence info to docplex self.assertTrue(i.is_absent()) @@ -810,6 +905,7 @@ def test_interval_var_fixed_length(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue(i.is_optional()) self.assertEqual(i.get_length(), (4, 4)) @@ -827,6 +923,7 @@ def test_interval_var_fixed_start_and_end(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertFalse(i.is_optional()) self.assertEqual(i.get_start(), (3, 3)) @@ -844,10 +941,14 @@ def get_model(self): def check_scalar_sequence_var(self, m, visitor): self.assertIn(id(m.seq), visitor.var_map) seq = visitor.var_map[id(m.seq)] + self.assertIs(visitor.pyomo_to_docplex[m.seq], seq) i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) ivs = seq.get_interval_variables() self.assertEqual(len(ivs), 3) @@ -914,6 +1015,8 @@ def test_start_before_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_before_start(i, i21, 0))) @@ -928,6 +1031,8 @@ def test_start_before_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_before_end(i, i21, 3))) @@ -942,6 +1047,8 @@ def test_end_before_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_before_start(i, i21, -2))) @@ -956,6 +1063,8 @@ def test_end_before_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_before_end(i, i21, 6))) @@ -970,6 +1079,8 @@ def test_start_at_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_at_start(i, i21, 0))) @@ -984,6 +1095,8 @@ def test_start_at_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_at_end(i, i21, 3))) @@ -998,6 +1111,8 @@ def test_end_at_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_at_start(i, i21, -2))) @@ -1012,6 +1127,8 @@ def test_end_at_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_at_end(i, i21, 6))) @@ -1036,6 +1153,10 @@ def test_indirection_before_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1062,6 +1183,10 @@ def test_indirection_after_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1089,6 +1214,10 @@ def test_indirection_at_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1116,6 +1245,10 @@ def test_before_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1141,6 +1274,10 @@ def test_after_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1166,6 +1303,10 @@ def test_at_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1200,6 +1341,13 @@ def test_double_indirection_before_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1237,6 +1385,13 @@ def test_double_indirection_after_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1272,6 +1427,13 @@ def test_double_indirection_at_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1319,6 +1481,8 @@ def param_rule(m, i): self.assertIn(id(m.a), visitor.var_map) x = visitor.var_map[id(m.x)] a = visitor.var_map[id(m.a)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a], a) self.assertTrue(expr[1].equals(cp.element([2, 4, 6], 0 + 1 * (x - 1) // 2) / a)) @@ -1346,6 +1510,8 @@ def test_spans(self): self.assertIn(id(m.whole_enchilada), visitor.var_map) whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + iv = {} for i in [1, 2, 3]: self.assertIn(id(m.iv[i]), visitor.var_map) @@ -1363,6 +1529,8 @@ def test_alternative(self): self.assertIn(id(m.whole_enchilada), visitor.var_map) whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + iv = {} for i in [1, 2, 3]: self.assertIn(id(m.iv[i]), visitor.var_map) @@ -1395,6 +1563,9 @@ def test_always_in(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) self.assertTrue( expr[1].equals( @@ -1422,6 +1593,7 @@ def test_always_in_single_pulse(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals(cp.always_in(cp.pulse(i, 3), interval=(0, 10), min=0, max=3)) @@ -1440,6 +1612,7 @@ def test_named_expression(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue(expr[1].equals(x**2 + 7)) @@ -1453,6 +1626,7 @@ def test_repeated_named_expression(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue(expr[1].equals(x**2 + 7 + (-1) * (8 * (x**2 + 7)))) @@ -1483,6 +1657,7 @@ def test_fixed_integer_var(self): self.assertIn(id(m.a[2]), visitor.var_map) a2 = visitor.var_map[id(m.a[2])] + self.assertIs(visitor.pyomo_to_docplex[m.a[2]], a2) self.assertTrue(expr[1].equals(3 + a2)) @@ -1497,6 +1672,7 @@ def test_fixed_boolean_var(self): self.assertIn(id(m.b2['b']), visitor.var_map) b2b = visitor.var_map[id(m.b2['b'])] + self.assertTrue(b2b.equals(visitor.pyomo_to_docplex[m.b2['b']] == 1)) self.assertTrue(expr[1].equals(cp.logical_or(False, cp.logical_and(True, b2b)))) @@ -1510,13 +1686,16 @@ def test_indirection_single_index(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) a = [] # only need indices 6, 7, and 8 from a, since that's what x is capable # of selecting. for idx in [6, 7, 8]: v = m.a[idx] self.assertIn(id(v), visitor.var_map) - a.append(visitor.var_map[id(v)]) + cpx_v = visitor.var_map[id(v)] + self.assertIs(visitor.pyomo_to_docplex[v], cpx_v) + a.append(cpx_v) # since x is between 6 and 8, we subtract 6 from it for it to be the # right index self.assertTrue(expr[1].equals(cp.element(a, 0 + 1 * (x - 6) // 1))) @@ -1534,8 +1713,10 @@ def test_indirection_multi_index_second_constant(self): for i in [6, 7, 8]: self.assertIn(id(m.z[i, 3]), visitor.var_map) z[i, 3] = visitor.var_map[id(m.z[i, 3])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, 3]], z[i, 3]) self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1556,8 +1737,11 @@ def test_indirection_multi_index_first_constant(self): for i in [6, 7, 8]: self.assertIn(id(m.z[3, i]), visitor.var_map) z[3, i] = visitor.var_map[id(m.z[3, i])] + self.assertIs(visitor.pyomo_to_docplex[m.z[3, i]], z[3, i]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1579,8 +1763,11 @@ def test_indirection_multi_index_neither_constant_same_var(self): for j in [6, 7, 8]: self.assertIn(id(m.z[i, j]), visitor.var_map) z[i, j] = visitor.var_map[id(m.z[i, j])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, j]], z[i, j]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1604,12 +1791,17 @@ def test_indirection_multi_index_neither_constant_diff_vars(self): z = {} for i in [6, 7, 8]: for j in [1, 3, 5]: - self.assertIn(id(m.z[i, 3]), visitor.var_map) + self.assertIn(id(m.z[i, j]), visitor.var_map) z[i, j] = visitor.var_map[id(m.z[i, j])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, j]], z[i, j]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIn(id(m.y), visitor.var_map) y = visitor.var_map[id(m.y)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) self.assertTrue( expr[1].equals( @@ -1634,10 +1826,14 @@ def test_indirection_expression_index(self): for i in range(1, 8): self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertIn(id(m.y), visitor.var_map) y = visitor.var_map[id(m.y)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) self.assertTrue( expr[1].equals( From 1e103090bb7724d5d0b7486e7f59a20f44c172d2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Jan 2024 15:52:00 -0700 Subject: [PATCH 0263/1178] NFC: black --- pyomo/contrib/cp/interval_var.py | 3 +-- pyomo/contrib/cp/repn/docplex_writer.py | 8 ++---- .../cp/scheduling_expr/scheduling_logic.py | 10 +++---- pyomo/contrib/cp/tests/test_docplex_walker.py | 26 +++++++++++-------- .../cp/tests/test_sequence_expressions.py | 14 ++++++---- 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index fb88ab14832..0e355d1847d 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -168,8 +168,7 @@ def __init__( optional=False, name=None, doc=None - ): - ... + ): ... def __init__(self, *args, **kwargs): _start_arg = kwargs.pop('start', None) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 1f6bcc347e7..3440b522e60 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -996,15 +996,11 @@ def _handle_predecessor_to_expression_node( return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) -def _handle_span_expression_node( - visitor, node, *args -): +def _handle_span_expression_node(visitor, node, *args): return _GENERAL, cp.span(args[0][1], [arg[1] for arg in args[1:]]) -def _handle_alternative_expression_node( - visitor, node, *args -): +def _handle_alternative_expression_node(visitor, node, *args): return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index a1f891a769f..fc9cefebf4d 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -22,6 +22,7 @@ class SpanExpression(NaryBooleanExpression): args: args (tuple): Child nodes, of type IntervalVar """ + def _to_string(self, values, verbose, smap): return "%s.spans(%s)" % (values[0], ", ".join(values[1:])) @@ -30,19 +31,18 @@ class AlternativeExpression(NaryBooleanExpression): """ TODO/ """ + def _to_string(self, values, verbose, smap): return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) - + def spans(*args): - """Creates a new SpanExpression - """ + """Creates a new SpanExpression""" return SpanExpression(list(_flattened(args))) def alternative(*args): - """Creates a new AlternativeExpression - """ + """Creates a new AlternativeExpression""" return AlternativeExpression(list(_flattened(args))) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 73b4fd8e00c..9d027296654 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -19,7 +19,7 @@ last_in_sequence, before_in_sequence, predecessor_to, - alternative + alternative, ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, @@ -1491,12 +1491,16 @@ def param_rule(m, i): class TestCPExpressionWalker_HierarchicalScheduling(CommonTest): def get_model(self): m = ConcreteModel() + def start_rule(m, i): - return 2*i + return 2 * i + def length_rule(m, i): return i - m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, - optional=True) + + m.iv = IntervalVar( + [1, 2, 3], start=start_rule, length=length_rule, optional=True + ) m.whole_enchilada = IntervalVar() return m @@ -1517,8 +1521,9 @@ def test_spans(self): self.assertIn(id(m.iv[i]), visitor.var_map) iv[i] = visitor.var_map[id(m.iv[i])] - self.assertTrue(expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in - [1, 2, 3]]))) + self.assertTrue( + expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) def test_alternative(self): m = self.get_model() @@ -1526,7 +1531,7 @@ def test_alternative(self): visitor = self.get_visitor() expr = visitor.walk_expression((e, e, 0)) - + self.assertIn(id(m.whole_enchilada), visitor.var_map) whole_enchilada = visitor.var_map[id(m.whole_enchilada)] self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) @@ -1536,10 +1541,9 @@ def test_alternative(self): self.assertIn(id(m.iv[i]), visitor.var_map) iv[i] = visitor.var_map[id(m.iv[i])] - self.assertTrue(expr[1].equals(cp.alternative(whole_enchilada, [iv[i] - for i in - [1, 2, - 3]]))) + self.assertTrue( + expr[1].equals(cp.alternative(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) @unittest.skipIf(not docplex_available, "docplex is not available") diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 93a283c43d1..218a4c0e1a0 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -16,7 +16,7 @@ AlternativeExpression, SpanExpression, alternative, - spans + spans, ) from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( NoOverlapExpression, @@ -113,12 +113,16 @@ def test_predecessor_in_sequence(self): class TestHierarchicalSchedulingExpressions(unittest.TestCase): def make_model(self): m = ConcreteModel() + def start_rule(m, i): - return 2*i + return 2 * i + def length_rule(m, i): return i - m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, - optional=True) + + m.iv = IntervalVar( + [1, 2, 3], start=start_rule, length=length_rule, optional=True + ) m.whole_enchilada = IntervalVar() return m @@ -137,7 +141,7 @@ def test_spans(self): m = self.make_model() e = spans(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) self.check_span_expression(m, e) - + def test_spans_method(self): m = self.make_model() e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) From bf0a54d5b8e97326caacad7363e35dd7a83ec4ff Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 10:52:46 -0700 Subject: [PATCH 0264/1178] Adding function for debugging infeasible CPs without having to mess with the docplex model --- pyomo/contrib/cp/debugging.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pyomo/contrib/cp/debugging.py diff --git a/pyomo/contrib/cp/debugging.py b/pyomo/contrib/cp/debugging.py new file mode 100644 index 00000000000..41c4d208de6 --- /dev/null +++ b/pyomo/contrib/cp/debugging.py @@ -0,0 +1,29 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.opt import WriterFactory + + +def write_conflict_set(m, filename): + """ + For debugging infeasible CPs: writes the conflict set found by CP optimizer + to a file with the specified filename. + + Args: + m: Pyomo CP model + filename: string filename + """ + + cpx_mod, var_map = WriterFactory('docplex_model').write( + m, symbolic_solver_labels=True + ) + conflict = cpx_mod.refine_conflict() + conflict.write(filename) From 462457b38b501922f10fcdf26928a78124b31b9c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 31 Jan 2024 16:57:30 -0700 Subject: [PATCH 0265/1178] Fix backwards compatibility --- pyomo/contrib/solver/base.py | 39 +++++------------------------------ pyomo/contrib/solver/ipopt.py | 4 ++-- pyomo/contrib/solver/util.py | 2 +- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 98fa60b722f..8aca11d2f0a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,6 +349,8 @@ def _map_config( load_solutions, symbolic_solver_labels, timelimit, + # Report timing is no longer a valid option. We now always return a + # timer object that can be inspected. report_timing, raise_exception_on_nonoptimal_result, solver_io, @@ -356,6 +358,7 @@ def _map_config( logfile, keepfiles, solnfile, + options, ): """Map between legacy and new interface configuration options""" self.config = self.config() @@ -363,7 +366,7 @@ def _map_config( self.config.load_solutions = load_solutions self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit - self.config.report_timing = report_timing + self.config.solver_options.set_value(options) # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" self.config.raise_exception_on_nonoptimal_result = ( @@ -483,12 +486,9 @@ def solve( logfile, keepfiles, solnfile, + options, ) - original_options = self.options - if options is not None: - self.options = options - results: Results = super().solve(model) legacy_results, legacy_soln = self._map_results(model, results) @@ -497,7 +497,6 @@ def solve( ) self.config = original_config - self.options = original_options return legacy_results @@ -526,31 +525,3 @@ def license_is_valid(self) -> bool: """ return bool(self.available()) - - @property - def options(self): - """ - Read the options for the dictated solver. - - NOTE: Only the set of solvers for which the LegacySolverWrapper is compatible - are accounted for within this property. - Not all solvers are currently covered by this backwards compatibility - class. - """ - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, 'solver_options'): - return getattr(self, 'solver_options') - raise NotImplementedError('Could not find the correct options') - - @options.setter - def options(self, val): - """ - Set the options for the dictated solver. - """ - found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, 'solver_options'): - setattr(self, 'solver_options', val) - found = True - if not found: - raise NotImplementedError('Could not find the correct options') diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 7c2a3f471e3..49cb0430e32 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -17,7 +17,7 @@ from typing import Mapping, Optional, Sequence from pyomo.common import Executable -from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat +from pyomo.common.config import ConfigValue, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer @@ -285,7 +285,7 @@ def solve(self, model, **kwds): f'Solver {self.__class__} is not available ({avail}).' ) # Update configuration options, based on keywords passed to solve - config: ipoptConfig = self.config(value=kwds) + config: ipoptConfig = self.config(value=kwds, preserve_implicit=True) if config.threads: logger.log( logging.WARNING, diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index 727d9c354e2..f8641b06c50 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -41,7 +41,7 @@ def check_optimal_termination(results): # Look at the original version of this function to make that happen. """ This function returns True if the termination condition for the solver - is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + is 'optimal'. Parameters ---------- From a0ad77e40b74dbaf3bfcf445eabe13d99b9d84af Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 31 Jan 2024 17:37:28 -0700 Subject: [PATCH 0266/1178] Add timing information to legacy results wrapper --- pyomo/contrib/solver/base.py | 4 ++++ pyomo/contrib/solver/ipopt.py | 1 + 2 files changed, 5 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8aca11d2f0a..1948bf8bf1b 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -444,6 +444,10 @@ def _solution_handler( legacy_soln.variable['Rc'] = val legacy_results.solution.insert(legacy_soln) + # Timing info was not originally on the legacy results, but we want + # to make it accessible to folks who are utilizing the backwards + # compatible version. + legacy_results.timing_info = results.timing_info if delete_legacy_soln: legacy_results.solution.delete(0) return legacy_results diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 49cb0430e32..7f62d67d38e 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -441,6 +441,7 @@ def solve(self, model, **kwds): results.timing_info.wall_time = ( end_timestamp - start_timestamp ).total_seconds() + results.timing_info.timer = timer return results def _parse_ipopt_output(self, stream: io.StringIO): From 248ffd523a2b7d74464b0a94b48ad311a3279848 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 31 Jan 2024 17:51:47 -0700 Subject: [PATCH 0267/1178] Add more base unit tets --- pyomo/contrib/solver/tests/unit/test_base.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 2d158025903..5531e0530fc 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -168,4 +168,20 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): - pass + def test_class_method_list(self): + expected_list = [ + 'available', + 'license_is_valid', + 'solve' + ] + method_list = [ + method for method in dir(base.LegacySolverWrapper) if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_context_manager(self): + with base.LegacySolverWrapper() as instance: + with self.assertRaises(AttributeError) as context: + instance.available() + + From a2a5513a4d517e088b3970f83fb27faef0fbe7c7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 1 Feb 2024 16:00:08 -0700 Subject: [PATCH 0268/1178] Add LegacySolverWrapper tests --- pyomo/contrib/solver/base.py | 14 ++- pyomo/contrib/solver/config.py | 11 ++- pyomo/contrib/solver/ipopt.py | 11 +-- pyomo/contrib/solver/tests/unit/test_base.py | 98 ++++++++++++++++++-- 4 files changed, 116 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 1948bf8bf1b..42524296d74 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -21,6 +21,7 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError +from pyomo.common.deprecation import deprecation_warning from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -378,8 +379,17 @@ def _map_config( raise NotImplementedError('Still working on this') if logfile is not None: raise NotImplementedError('Still working on this') - if 'keepfiles' in self.config: - self.config.keepfiles = keepfiles + if keepfiles or 'keepfiles' in self.config: + cwd = os.getcwd() + deprecation_warning( + "`keepfiles` has been deprecated in the new solver interface. " + "Use `working_dir` instead to designate a directory in which " + f"files should be generated and saved. Setting `working_dir` to `{cwd}`.", + version='6.7.1.dev0', + ) + self.config.working_dir = cwd + # I believe this currently does nothing; however, it is unclear what + # our desired behavior is for this. if solnfile is not None: if 'filename' in self.config: filename = os.path.splitext(solnfile)[0] diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 4c81d31a820..d5921c526b0 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -58,6 +58,14 @@ def __init__( description="If True, the solver output gets logged.", ), ) + self.working_dir: str = self.declare( + 'working_dir', + ConfigValue( + domain=str, + default=None, + description="The directory in which generated files should be saved. This replaced the `keepfiles` option.", + ), + ) self.load_solutions: bool = self.declare( 'load_solutions', ConfigValue( @@ -79,7 +87,8 @@ def __init__( ConfigValue( domain=bool, default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + description="If True, the names given to the solver will reflect the names of the Pyomo components." + "Cannot be changed after set_instance is called.", ), ) self.timer: HierarchicalTimer = self.declare( diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 7f62d67d38e..4c4b932381d 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -68,11 +68,6 @@ def __init__( self.executable = self.declare( 'executable', ConfigValue(default=Executable('ipopt')) ) - # TODO: Add in a deprecation here for keepfiles - # M.B.: Is the above TODO still relevant? - self.temp_dir: str = self.declare( - 'temp_dir', ConfigValue(domain=str, default=None) - ) self.writer_config = self.declare( 'writer_config', ConfigValue(default=NLWriter.CONFIG()) ) @@ -262,7 +257,7 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: raise ValueError( - 'Pyomo generates the ipopt options file as part of the solve method. ' + 'Pyomo generates the ipopt options file as part of the `solve` method. ' 'Add all options to ipopt.config.solver_options instead.' ) if ( @@ -298,10 +293,10 @@ def solve(self, model, **kwds): StaleFlagManager.mark_all_as_stale() results = ipoptResults() with TempfileManager.new_context() as tempfile: - if config.temp_dir is None: + if config.working_dir is None: dname = tempfile.mkdtemp() else: - dname = config.temp_dir + dname = config.working_dir if not os.path.exists(dname): os.mkdir(dname) basename = os.path.join(dname, model.name) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 5531e0530fc..00e38d9ac59 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -9,7 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import os + from pyomo.common import unittest +from pyomo.common.config import ConfigDict from pyomo.contrib.solver import base @@ -169,13 +172,11 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): def test_class_method_list(self): - expected_list = [ - 'available', - 'license_is_valid', - 'solve' - ] + expected_list = ['available', 'license_is_valid', 'solve'] method_list = [ - method for method in dir(base.LegacySolverWrapper) if method.startswith('_') is False + method + for method in dir(base.LegacySolverWrapper) + if method.startswith('_') is False ] self.assertEqual(sorted(expected_list), sorted(method_list)) @@ -184,4 +185,87 @@ def test_context_manager(self): with self.assertRaises(AttributeError) as context: instance.available() - + def test_map_config(self): + # Create a fake/empty config structure that can be added to an empty + # instance of LegacySolverWrapper + self.config = ConfigDict(implicit=True) + self.config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + instance = base.LegacySolverWrapper() + instance.config = self.config + instance._map_config( + True, False, False, 20, True, False, None, None, None, False, None, None + ) + self.assertTrue(instance.config.tee) + self.assertFalse(instance.config.load_solutions) + self.assertEqual(instance.config.time_limit, 20) + # Report timing shouldn't be created because it no longer exists + with self.assertRaises(AttributeError) as context: + print(instance.config.report_timing) + # Keepfiles should not be created because we did not declare keepfiles on + # the original config + with self.assertRaises(AttributeError) as context: + print(instance.config.keepfiles) + # We haven't implemented solver_io, suffixes, or logfile + with self.assertRaises(NotImplementedError) as context: + instance._map_config( + False, + False, + False, + 20, + False, + False, + None, + None, + '/path/to/bogus/file', + False, + None, + None, + ) + with self.assertRaises(NotImplementedError) as context: + instance._map_config( + False, + False, + False, + 20, + False, + False, + None, + '/path/to/bogus/file', + None, + False, + None, + None, + ) + with self.assertRaises(NotImplementedError) as context: + instance._map_config( + False, + False, + False, + 20, + False, + False, + '/path/to/bogus/file', + None, + None, + False, + None, + None, + ) + # If they ask for keepfiles, we redirect them to working_dir + instance._map_config( + False, False, False, 20, False, False, None, None, None, True, None, None + ) + self.assertEqual(instance.config.working_dir, os.getcwd()) + with self.assertRaises(AttributeError) as context: + print(instance.config.keepfiles) + + def test_map_results(self): + # Unclear how to test this + pass + + def test_solution_handler(self): + # Unclear how to test this + pass From 93a04098e02a7c6c705d13a353565fa9ebe77db8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 1 Feb 2024 16:06:46 -0700 Subject: [PATCH 0269/1178] Starting to draft correct mapping for disaggregated vars--this is totally broken --- pyomo/gdp/plugins/hull.py | 45 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 3d2be2f9e15..c7a005bb4ea 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -599,6 +599,9 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) + parent_block = var.parent_block() + disaggregated_var_map = self._get_disaggregated_var_map(parent_block) + print("Adding bounds constraints for local var '%s'" % var) # TODO: This gets mapped in a place where we can't find it if we ask # for it from the local var itself. @@ -610,7 +613,7 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, 'lb', 'ub', obj.indicator_var.get_associated_binary(), - transBlock, + disaggregated_var_map, ) var_substitute_map = dict( @@ -647,10 +650,8 @@ def _declare_disaggregated_var_bounds( lb_idx, ub_idx, var_free_indicator, - transBlock=None, + disaggregated_var_map, ): - # If transBlock is None then this is a disaggregated variable for - # multiple Disjuncts and we will handle the mappings separately. lb = original_var.lb ub = original_var.ub if lb is None or ub is None: @@ -669,13 +670,18 @@ def _declare_disaggregated_var_bounds( bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) # store the mappings from variables to their disaggregated selves on - # the transformation block. - if transBlock is not None: - transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ - original_var - ] = disaggregatedVar - transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var - transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint + # the transformation block + disaggregated_var_map['disaggregatedVar'][disjunct][ + original_var] = disaggregatedVar + disaggregated_var_map['srcVar'][disaggregatedVar] = original_var + bigMConstraintMap[disaggregatedVar] = bigmConstraint + + # if transBlock is not None: + # transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ + # original_var + # ] = disaggregatedVar + # transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var + # transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint def _get_local_var_list(self, parent_disjunct): # Add or retrieve Suffix from parent_disjunct so that, if this is @@ -916,7 +922,7 @@ def get_src_var(self, disaggregated_var): Parameters ---------- - disaggregated_var: a Var which was created by the hull + disaggregated_var: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) @@ -925,17 +931,14 @@ def get_src_var(self, disaggregated_var): "'%s' does not appear to be a " "disaggregated variable" % disaggregated_var.name ) - # There are two possibilities: It is declared on a Disjunct - # transformation Block, or it is declared on the parent of a Disjunct - # transformation block (if it is a single variable for multiple - # Disjuncts the original doesn't appear in) + # We always put a dictionary called '_disaggregatedVarMap' on the parent + # block of the variable. If it's not there, then this probably isn't a + # disaggregated Var (or if it is it's a developer error). Similarly, if + # the var isn't in the dictionary, if we're doing what we should, then + # it's not a disaggregated var. transBlock = disaggregated_var.parent_block() if not hasattr(transBlock, '_disaggregatedVarMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise + raise GDP_Error(msg) try: return transBlock._disaggregatedVarMap['srcVar'][disaggregated_var] except: From 2c8a4d818c70c7088a4f45d6b5fa23b07f028a75 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 1 Feb 2024 16:48:59 -0700 Subject: [PATCH 0270/1178] Backwards compatibility; add tests --- pyomo/contrib/solver/tests/unit/test_util.py | 43 ++++++++++++++++- pyomo/contrib/solver/util.py | 50 +++++++++++++++----- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py index 9bf92af72cf..8a8a0221362 100644 --- a/pyomo/contrib/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -11,9 +11,18 @@ from pyomo.common import unittest import pyomo.environ as pyo -from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective +from pyomo.contrib.solver.util import ( + collect_vars_and_named_exprs, + get_objective, + check_optimal_termination, + assert_optimal_termination, + SolverStatus, + LegacyTerminationCondition, +) +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition from typing import Callable from pyomo.common.gsl import find_GSL +from pyomo.opt.results import SolverResults class TestGenericUtils(unittest.TestCase): @@ -73,3 +82,35 @@ def test_get_objective_raise(self): model.OBJ2 = pyo.Objective(expr=model.x[1] - 4 * model.x[2]) with self.assertRaises(ValueError): get_objective(model) + + def test_check_optimal_termination_new_interface(self): + results = Results() + results.solution_status = SolutionStatus.optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + # Both items satisfied + self.assertTrue(check_optimal_termination(results)) + # Termination condition not satisfied + results.termination_condition = TerminationCondition.iterationLimit + self.assertFalse(check_optimal_termination(results)) + # Both not satisfied + results.solution_status = SolutionStatus.noSolution + self.assertFalse(check_optimal_termination(results)) + + def test_check_optimal_termination_condition_legacy_interface(self): + results = SolverResults() + results.solver.status = SolverStatus.ok + results.solver.termination_condition = LegacyTerminationCondition.optimal + self.assertTrue(check_optimal_termination(results)) + results.solver.termination_condition = LegacyTerminationCondition.unknown + self.assertFalse(check_optimal_termination(results)) + results.solver.termination_condition = SolverStatus.aborted + self.assertFalse(check_optimal_termination(results)) + + # TODO: Left off here; need to make these tests + def test_assert_optimal_termination_new_interface(self): + pass + + def test_assert_optimal_termination_legacy_interface(self): + pass diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index f8641b06c50..807d66f569e 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -22,10 +22,20 @@ from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant +from pyomo.opt.results.solver import ( + SolverStatus, + TerminationCondition as LegacyTerminationCondition, +) + + from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus def get_objective(block): + """ + Get current active objective on a block. If there is more than one active, + return an error. + """ obj = None for o in block.component_data_objects( Objective, descend_into=True, active=True, sort=True @@ -37,8 +47,6 @@ def get_objective(block): def check_optimal_termination(results): - # TODO: Make work for legacy and new results objects. - # Look at the original version of this function to make that happen. """ This function returns True if the termination condition for the solver is 'optimal'. @@ -51,11 +59,21 @@ def check_optimal_termination(results): ------- `bool` """ - if results.solution_status == SolutionStatus.optimal and ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): - return True + if hasattr(results, 'solution_status'): + if results.solution_status == SolutionStatus.optimal and ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): + return True + else: + if results.solver.status == SolverStatus.ok and ( + results.solver.termination_condition == LegacyTerminationCondition.optimal + or results.solver.termination_condition + == LegacyTerminationCondition.locallyOptimal + or results.solver.termination_condition + == LegacyTerminationCondition.globallyOptimal + ): + return True return False @@ -70,12 +88,20 @@ def assert_optimal_termination(results): results : Pyomo Results object returned from solver.solve """ if not check_optimal_termination(results): - msg = ( - 'Solver failed to return an optimal solution. ' - 'Solution status: {}, Termination condition: {}'.format( - results.solution_status, results.termination_condition + if hasattr(results, 'solution_status'): + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solution status: {}, Termination condition: {}'.format( + results.solution_status, results.termination_condition + ) + ) + else: + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solver status: {}, Termination condition: {}'.format( + results.solver.status, results.solver.termination_condition + ) ) - ) raise RuntimeError(msg) From d224bbe4df9f0a94010defce3f266b789590ba87 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 19:48:45 -0500 Subject: [PATCH 0271/1178] Remove custom PyROS `ConfigDict` interfaces --- pyomo/contrib/pyros/pyros.py | 325 +++++------------------------------ 1 file changed, 44 insertions(+), 281 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 829184fc70c..f266b7451e6 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -13,7 +13,13 @@ import logging from textwrap import indent, dedent, wrap from pyomo.common.collections import Bunch, ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + document_kwargs_from_configdict, + In, + NonNegativeFloat, +) from pyomo.core.base.block import Block from pyomo.core.expr import value from pyomo.core.base.var import Var, _VarData @@ -147,68 +153,6 @@ def __call__(self, obj): return ans -class PyROSConfigValue(ConfigValue): - """ - Subclass of ``common.collections.ConfigValue``, - with a few attributes added to facilitate documentation - of the PyROS solver. - An instance of this class is used for storing and - documenting an argument to the PyROS solver. - - Attributes - ---------- - is_optional : bool - Argument is optional. - document_default : bool, optional - Document the default value of the argument - in any docstring generated from this instance, - or a `ConfigDict` object containing this instance. - dtype_spec_str : None or str, optional - String documenting valid types for this argument. - If `None` is provided, then this string is automatically - determined based on the `domain` argument to the - constructor. - - NOTES - ----- - Cleaner way to access protected attributes - (particularly _doc, _description) inherited from ConfigValue? - - """ - - def __init__( - self, - default=None, - domain=None, - description=None, - doc=None, - visibility=0, - is_optional=True, - document_default=True, - dtype_spec_str=None, - ): - """Initialize self (see class docstring).""" - - # initialize base class attributes - super(self.__class__, self).__init__( - default=default, - domain=domain, - description=description, - doc=doc, - visibility=visibility, - ) - - self.is_optional = is_optional - self.document_default = document_default - - if dtype_spec_str is None: - self.dtype_spec_str = self.domain_name() - # except AttributeError: - # self.dtype_spec_str = repr(self._domain) - else: - self.dtype_spec_str = dtype_spec_str - - def pyros_config(): CONFIG = ConfigDict('PyROS') @@ -217,7 +161,7 @@ def pyros_config(): # ================================================ CONFIG.declare( 'time_limit', - PyROSConfigValue( + ConfigValue( default=None, domain=NonNegativeFloat, doc=( @@ -227,14 +171,11 @@ def pyros_config(): If `None` is provided, then no time limit is enforced. """ ), - is_optional=True, - document_default=False, - dtype_spec_str="None or NonNegativeFloat", ), ) CONFIG.declare( 'keepfiles', - PyROSConfigValue( + ConfigValue( default=False, domain=bool, description=( @@ -245,25 +186,19 @@ def pyros_config(): must also be specified. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( 'tee', - PyROSConfigValue( + ConfigValue( default=False, domain=bool, description="Output subordinate solver logs for all subproblems.", - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( 'load_solution', - PyROSConfigValue( + ConfigValue( default=True, domain=bool, description=( @@ -272,9 +207,6 @@ def pyros_config(): provided. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) @@ -283,27 +215,25 @@ def pyros_config(): # ================================================ CONFIG.declare( "first_stage_variables", - PyROSConfigValue( + ConfigValue( default=[], domain=InputDataStandardizer(Var, _VarData), description="First-stage (or design) variables.", - is_optional=False, - dtype_spec_str="list of Var", + visibility=1, ), ) CONFIG.declare( "second_stage_variables", - PyROSConfigValue( + ConfigValue( default=[], domain=InputDataStandardizer(Var, _VarData), description="Second-stage (or control) variables.", - is_optional=False, - dtype_spec_str="list of Var", + visibility=1, ), ) CONFIG.declare( "uncertain_params", - PyROSConfigValue( + ConfigValue( default=[], domain=InputDataStandardizer(Param, _ParamData), description=( @@ -313,13 +243,12 @@ def pyros_config(): objects should be set to True. """ ), - is_optional=False, - dtype_spec_str="list of Param", + visibility=1, ), ) CONFIG.declare( "uncertainty_set", - PyROSConfigValue( + ConfigValue( default=None, domain=uncertainty_sets, description=( @@ -329,28 +258,25 @@ def pyros_config(): to be robust. """ ), - is_optional=False, - dtype_spec_str="UncertaintySet", + visibility=1, ), ) CONFIG.declare( "local_solver", - PyROSConfigValue( + ConfigValue( default=None, domain=SolverResolvable(), description="Subordinate local NLP solver.", - is_optional=False, - dtype_spec_str="Solver", + visibility=1, ), ) CONFIG.declare( "global_solver", - PyROSConfigValue( + ConfigValue( default=None, domain=SolverResolvable(), description="Subordinate global NLP solver.", - is_optional=False, - dtype_spec_str="Solver", + visibility=1, ), ) # ================================================ @@ -358,7 +284,7 @@ def pyros_config(): # ================================================ CONFIG.declare( "objective_focus", - PyROSConfigValue( + ConfigValue( default=ObjectiveType.nominal, domain=ValidEnum(ObjectiveType), description=( @@ -388,14 +314,11 @@ def pyros_config(): feasibility is guaranteed. """ ), - is_optional=True, - document_default=False, - dtype_spec_str="ObjectiveType", ), ) CONFIG.declare( "nominal_uncertain_param_vals", - PyROSConfigValue( + ConfigValue( default=[], domain=list, doc=( @@ -407,14 +330,11 @@ def pyros_config(): objects specified through `uncertain_params` are chosen. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="list of float", ), ) CONFIG.declare( "decision_rule_order", - PyROSConfigValue( + ConfigValue( default=0, domain=In([0, 1, 2]), description=( @@ -437,14 +357,11 @@ def pyros_config(): - 2: quadratic recourse """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "solve_master_globally", - PyROSConfigValue( + ConfigValue( default=False, domain=bool, doc=( @@ -460,14 +377,11 @@ def pyros_config(): by PyROS. Otherwise, only robust feasibility is guaranteed. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "max_iter", - PyROSConfigValue( + ConfigValue( default=-1, domain=PositiveIntOrMinusOne, description=( @@ -476,14 +390,11 @@ def pyros_config(): limit is enforced. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="int", ), ) CONFIG.declare( "robust_feasibility_tolerance", - PyROSConfigValue( + ConfigValue( default=1e-4, domain=NonNegativeFloat, description=( @@ -492,14 +403,11 @@ def pyros_config(): constraint violations during the GRCS separation step. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "separation_priority_order", - PyROSConfigValue( + ConfigValue( default={}, domain=dict, doc=( @@ -514,14 +422,11 @@ def pyros_config(): priority. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "progress_logger", - PyROSConfigValue( + ConfigValue( default=default_pyros_solver_logger, domain=a_logger, doc=( @@ -534,14 +439,11 @@ def pyros_config(): object of level ``logging.INFO``. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="str or logging.Logger", ), ) CONFIG.declare( "backup_local_solvers", - PyROSConfigValue( + ConfigValue( default=[], domain=SolverResolvable(), doc=( @@ -551,14 +453,11 @@ def pyros_config(): to solve a subproblem to an acceptable termination condition. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", ), ) CONFIG.declare( "backup_global_solvers", - PyROSConfigValue( + ConfigValue( default=[], domain=SolverResolvable(), doc=( @@ -568,14 +467,11 @@ def pyros_config(): to solve a subproblem to an acceptable termination condition. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", ), ) CONFIG.declare( "subproblem_file_directory", - PyROSConfigValue( + ConfigValue( default=None, domain=str, description=( @@ -587,9 +483,6 @@ def pyros_config(): provided. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="None, str, or path-like", ), ) @@ -598,7 +491,7 @@ def pyros_config(): # ================================================ CONFIG.declare( "bypass_local_separation", - PyROSConfigValue( + ConfigValue( default=False, domain=bool, description=( @@ -611,14 +504,11 @@ def pyros_config(): can quickly solve separation subproblems to global optimality. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "bypass_global_separation", - PyROSConfigValue( + ConfigValue( default=False, domain=bool, doc=( @@ -635,14 +525,11 @@ def pyros_config(): optimality. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "p_robustness", - PyROSConfigValue( + ConfigValue( default={}, domain=dict, doc=( @@ -660,9 +547,6 @@ def pyros_config(): the nominal parameter realization. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) @@ -836,6 +720,13 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): logger.log(msg=f" {key}={val!r}", **log_kwargs) logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + @document_kwargs_from_configdict( + config=CONFIG, + section="Keyword Arguments", + indent_spacing=4, + width=72, + visibility=0, + ) def solve( self, model, @@ -1085,131 +976,3 @@ def solve( config.progress_logger.info("=" * self._LOG_LINE_LENGTH) return return_soln - - -def _generate_filtered_docstring(): - """ - Add Numpy-style 'Keyword arguments' section to `PyROS.solve()` - docstring. - """ - cfg = PyROS.CONFIG() - - # mandatory args already documented - exclude_args = [ - "first_stage_variables", - "second_stage_variables", - "uncertain_params", - "uncertainty_set", - "local_solver", - "global_solver", - ] - - indent_by = 8 - width = 72 - before = PyROS.solve.__doc__ - section_name = "Keyword Arguments" - - indent_str = ' ' * indent_by - wrap_width = width - indent_by - cfg = pyros_config() - - arg_docs = [] - - def wrap_doc(doc, indent_by, width): - """ - Wrap a string, accounting for paragraph - breaks ('\n\n') and bullet points (paragraphs - which, when dedented, are such that each line - starts with '- ' or ' '). - """ - paragraphs = doc.split("\n\n") - wrapped_pars = [] - for par in paragraphs: - lines = dedent(par).split("\n") - has_bullets = all( - line.startswith("- ") or line.startswith(" ") - for line in lines - if line != "" - ) - if has_bullets: - # obtain strings of each bullet point - # (dedented, bullet dash and bullet indent removed) - bullet_groups = [] - new_group = False - group = "" - for line in lines: - new_group = line.startswith("- ") - if new_group: - bullet_groups.append(group) - group = "" - new_line = line[2:] - group += f"{new_line}\n" - if group != "": - # ensure last bullet not skipped - bullet_groups.append(group) - - # first entry is just ''; remove - bullet_groups = bullet_groups[1:] - - # wrap each bullet point, then add bullet - # and indents as necessary - wrapped_groups = [] - for group in bullet_groups: - wrapped_groups.append( - "\n".join( - f"{'- ' if idx == 0 else ' '}{line}" - for idx, line in enumerate( - wrap(group, width - 2 - indent_by) - ) - ) - ) - - # now combine bullets into single 'paragraph' - wrapped_pars.append( - indent("\n".join(wrapped_groups), prefix=' ' * indent_by) - ) - else: - wrapped_pars.append( - indent( - "\n".join(wrap(dedent(par), width=width - indent_by)), - prefix=' ' * indent_by, - ) - ) - - return "\n\n".join(wrapped_pars) - - section_header = indent(f"{section_name}\n" + "-" * len(section_name), indent_str) - for key, itm in cfg._data.items(): - if key in exclude_args: - continue - arg_name = key - arg_dtype = itm.dtype_spec_str - - if itm.is_optional: - if itm.document_default: - optional_str = f", default={repr(itm._default)}" - else: - optional_str = ", optional" - else: - optional_str = "" - - arg_header = f"{indent_str}{arg_name} : {arg_dtype}{optional_str}" - - # dedented_doc_str = dedent(itm.doc).replace("\n", ' ').strip() - if itm._doc is not None: - raw_arg_desc = itm._doc - else: - raw_arg_desc = itm._description - - arg_description = wrap_doc( - raw_arg_desc, width=wrap_width, indent_by=indent_by + 4 - ) - - arg_docs.append(f"{arg_header}\n{arg_description}") - - kwargs_section_doc = "\n".join([section_header] + arg_docs) - - return f"{before}\n{kwargs_section_doc}\n" - - -PyROS.solve.__doc__ = _generate_filtered_docstring() From d5fc7cbac76727c4e858aec9b6c360e2e42088bc Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 20:10:53 -0500 Subject: [PATCH 0272/1178] Create new module for config objects --- pyomo/contrib/pyros/config.py | 493 ++++++++++++++++++++++++++++++++++ pyomo/contrib/pyros/pyros.py | 481 +-------------------------------- 2 files changed, 498 insertions(+), 476 deletions(-) create mode 100644 pyomo/contrib/pyros/config.py diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py new file mode 100644 index 00000000000..1dc1608ab16 --- /dev/null +++ b/pyomo/contrib/pyros/config.py @@ -0,0 +1,493 @@ +""" +Interfaces for managing PyROS solver options. +""" + + +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + In, + NonNegativeFloat, +) +from pyomo.core.base import ( + Var, + _VarData, +) +from pyomo.core.base.param import ( + Param, + _ParamData, +) +from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.util import ( + a_logger, + ObjectiveType, + setup_pyros_logger, + ValidEnum, +) +from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets + + +default_pyros_solver_logger = setup_pyros_logger() + + +def NonNegIntOrMinusOne(obj): + ''' + if obj is a non-negative int, return the non-negative int + if obj is -1, return -1 + else, error + ''' + ans = int(obj) + if ans != float(obj) or (ans < 0 and ans != -1): + raise ValueError("Expected non-negative int, but received %s" % (obj,)) + return ans + + +def PositiveIntOrMinusOne(obj): + ''' + if obj is a positive int, return the int + if obj is -1, return -1 + else, error + ''' + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError("Expected positive int, but received %s" % (obj,)) + return ans + + +class SolverResolvable(object): + def __call__(self, obj): + ''' + if obj is a string, return the Solver object for that solver name + if obj is a Solver object, return a copy of the Solver + if obj is a list, and each element of list is solver resolvable, + return list of solvers + ''' + if isinstance(obj, str): + return SolverFactory(obj.lower()) + elif callable(getattr(obj, "solve", None)): + return obj + elif isinstance(obj, list): + return [self(o) for o in obj] + else: + raise ValueError( + "Expected a Pyomo solver or string object, " + "instead received {0}".format(obj.__class__.__name__) + ) + + +class InputDataStandardizer(object): + def __init__(self, ctype, cdatatype): + self.ctype = ctype + self.cdatatype = cdatatype + + def __call__(self, obj): + if isinstance(obj, self.ctype): + return list(obj.values()) + if isinstance(obj, self.cdatatype): + return [obj] + ans = [] + for item in obj: + ans.extend(self.__call__(item)) + for _ in ans: + assert isinstance(_, self.cdatatype) + return ans + + +def pyros_config(): + CONFIG = ConfigDict('PyROS') + + # ================================================ + # === Options common to all solvers + # ================================================ + CONFIG.declare( + 'time_limit', + ConfigValue( + default=None, + domain=NonNegativeFloat, + doc=( + """ + Wall time limit for the execution of the PyROS solver + in seconds (including time spent by subsolvers). + If `None` is provided, then no time limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + 'keepfiles', + ConfigValue( + default=False, + domain=bool, + description=( + """ + Export subproblems with a non-acceptable termination status + for debugging purposes. + If True is provided, then the argument `subproblem_file_directory` + must also be specified. + """ + ), + ), + ) + CONFIG.declare( + 'tee', + ConfigValue( + default=False, + domain=bool, + description="Output subordinate solver logs for all subproblems.", + ), + ) + CONFIG.declare( + 'load_solution', + ConfigValue( + default=True, + domain=bool, + description=( + """ + Load final solution(s) found by PyROS to the deterministic model + provided. + """ + ), + ), + ) + + # ================================================ + # === Required User Inputs + # ================================================ + CONFIG.declare( + "first_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, _VarData), + description="First-stage (or design) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "second_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, _VarData), + description="Second-stage (or control) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "uncertain_params", + ConfigValue( + default=[], + domain=InputDataStandardizer(Param, _ParamData), + description=( + """ + Uncertain model parameters. + The `mutable` attribute for all uncertain parameter + objects should be set to True. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "uncertainty_set", + ConfigValue( + default=None, + domain=uncertainty_sets, + description=( + """ + Uncertainty set against which the + final solution(s) returned by PyROS should be certified + to be robust. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "local_solver", + ConfigValue( + default=None, + domain=SolverResolvable(), + description="Subordinate local NLP solver.", + visibility=1, + ), + ) + CONFIG.declare( + "global_solver", + ConfigValue( + default=None, + domain=SolverResolvable(), + description="Subordinate global NLP solver.", + visibility=1, + ), + ) + # ================================================ + # === Optional User Inputs + # ================================================ + CONFIG.declare( + "objective_focus", + ConfigValue( + default=ObjectiveType.nominal, + domain=ValidEnum(ObjectiveType), + description=( + """ + Choice of objective focus to optimize in the master problems. + Choices are: `ObjectiveType.worst_case`, + `ObjectiveType.nominal`. + """ + ), + doc=( + """ + Objective focus for the master problems: + + - `ObjectiveType.nominal`: + Optimize the objective function subject to the nominal + uncertain parameter realization. + - `ObjectiveType.worst_case`: + Optimize the objective function subject to the worst-case + uncertain parameter realization. + + By default, `ObjectiveType.nominal` is chosen. + + A worst-case objective focus is required for certification + of robust optimality of the final solution(s) returned + by PyROS. + If a nominal objective focus is chosen, then only robust + feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "nominal_uncertain_param_vals", + ConfigValue( + default=[], + domain=list, + doc=( + """ + Nominal uncertain parameter realization. + Entries should be provided in an order consistent with the + entries of the argument `uncertain_params`. + If an empty list is provided, then the values of the `Param` + objects specified through `uncertain_params` are chosen. + """ + ), + ), + ) + CONFIG.declare( + "decision_rule_order", + ConfigValue( + default=0, + domain=In([0, 1, 2]), + description=( + """ + Order (or degree) of the polynomial decision rule functions used + for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + """ + ), + doc=( + """ + Order (or degree) of the polynomial decision rule functions used + for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + + Choices are: + + - 0: static recourse + - 1: affine recourse + - 2: quadratic recourse + """ + ), + ), + ) + CONFIG.declare( + "solve_master_globally", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + True to solve all master problems with the subordinate + global solver, False to solve all master problems with + the subordinate local solver. + Along with a worst-case objective focus + (see argument `objective_focus`), + solving the master problems to global optimality is required + for certification + of robust optimality of the final solution(s) returned + by PyROS. Otherwise, only robust feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "max_iter", + ConfigValue( + default=-1, + domain=PositiveIntOrMinusOne, + description=( + """ + Iteration limit. If -1 is provided, then no iteration + limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + "robust_feasibility_tolerance", + ConfigValue( + default=1e-4, + domain=NonNegativeFloat, + description=( + """ + Relative tolerance for assessing maximal inequality + constraint violations during the GRCS separation step. + """ + ), + ), + ) + CONFIG.declare( + "separation_priority_order", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + Mapping from model inequality constraint names + to positive integers specifying the priorities + of their corresponding separation subproblems. + A higher integer value indicates a higher priority. + Constraints not referenced in the `dict` assume + a priority of 0. + Separation subproblems are solved in order of decreasing + priority. + """ + ), + ), + ) + CONFIG.declare( + "progress_logger", + ConfigValue( + default=default_pyros_solver_logger, + domain=a_logger, + doc=( + """ + Logger (or name thereof) used for reporting PyROS solver + progress. If a `str` is specified, then ``progress_logger`` + is cast to ``logging.getLogger(progress_logger)``. + In the default case, `progress_logger` is set to + a :class:`pyomo.contrib.pyros.util.PreformattedLogger` + object of level ``logging.INFO``. + """ + ), + ), + ) + CONFIG.declare( + "backup_local_solvers", + ConfigValue( + default=[], + domain=SolverResolvable(), + doc=( + """ + Additional subordinate local NLP optimizers to invoke + in the event the primary local NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "backup_global_solvers", + ConfigValue( + default=[], + domain=SolverResolvable(), + doc=( + """ + Additional subordinate global NLP optimizers to invoke + in the event the primary global NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "subproblem_file_directory", + ConfigValue( + default=None, + domain=str, + description=( + """ + Directory to which to export subproblems not successfully + solved to an acceptable termination condition. + In the event ``keepfiles=True`` is specified, a str or + path-like referring to an existing directory must be + provided. + """ + ), + ), + ) + + # ================================================ + # === Advanced Options + # ================================================ + CONFIG.declare( + "bypass_local_separation", + ConfigValue( + default=False, + domain=bool, + description=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate global + solver(s) only. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer(s) provided + can quickly solve separation subproblems to global optimality. + """ + ), + ), + ) + CONFIG.declare( + "bypass_global_separation", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate local + solver(s) only. + If `True` is chosen, then robustness of the final solution(s) + returned by PyROS is not guaranteed, and a warning will + be issued at termination. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer provided + cannot tractably solve separation subproblems to global + optimality. + """ + ), + ), + ) + CONFIG.declare( + "p_robustness", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + This is an advanced option. + Add p-robustness constraints to all master subproblems. + If an empty dict is provided, then p-robustness constraints + are not added. + Otherwise, the dict must map a `str` of value ``'rho'`` + to a non-negative `float`. PyROS automatically + specifies ``1 + p_robustness['rho']`` + as an upper bound for the ratio of the + objective function value under any PyROS-sampled uncertain + parameter realization to the objective function under + the nominal parameter realization. + """ + ), + ), + ) + + return CONFIG diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index f266b7451e6..962ae79a436 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -11,23 +11,16 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging -from textwrap import indent, dedent, wrap +from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.collections import Bunch, ComponentSet -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - document_kwargs_from_configdict, - In, - NonNegativeFloat, -) from pyomo.core.base.block import Block from pyomo.core.expr import value -from pyomo.core.base.var import Var, _VarData -from pyomo.core.base.param import Param, _ParamData -from pyomo.core.base.objective import Objective, maximize -from pyomo.contrib.pyros.util import a_logger, time_code, get_main_elapsed_time +from pyomo.core.base.var import Var +from pyomo.core.base.objective import Objective +from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.config import pyros_config from pyomo.contrib.pyros.util import ( model_is_valid, recast_to_min_obj, @@ -35,7 +28,6 @@ add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, - ValidEnum, ObjectiveType, validate_uncertainty_set, identify_objective_functions, @@ -49,7 +41,6 @@ ) from pyomo.contrib.pyros.solve_data import ROSolveResults from pyomo.contrib.pyros.pyros_algorithm_methods import ROSolver_iterative_solve -from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets from pyomo.core.base import Constraint from datetime import datetime @@ -91,468 +82,6 @@ def _get_pyomo_version_info(): return {"Pyomo version": pyomo_version, "Commit hash": commit_hash} -def NonNegIntOrMinusOne(obj): - ''' - if obj is a non-negative int, return the non-negative int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans < 0 and ans != -1): - raise ValueError("Expected non-negative int, but received %s" % (obj,)) - return ans - - -def PositiveIntOrMinusOne(obj): - ''' - if obj is a positive int, return the int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError("Expected positive int, but received %s" % (obj,)) - return ans - - -class SolverResolvable(object): - def __call__(self, obj): - ''' - if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return a copy of the Solver - if obj is a list, and each element of list is solver resolvable, return list of solvers - ''' - if isinstance(obj, str): - return SolverFactory(obj.lower()) - elif callable(getattr(obj, "solve", None)): - return obj - elif isinstance(obj, list): - return [self(o) for o in obj] - else: - raise ValueError( - "Expected a Pyomo solver or string object, " - "instead received {1}".format(obj.__class__.__name__) - ) - - -class InputDataStandardizer(object): - def __init__(self, ctype, cdatatype): - self.ctype = ctype - self.cdatatype = cdatatype - - def __call__(self, obj): - if isinstance(obj, self.ctype): - return list(obj.values()) - if isinstance(obj, self.cdatatype): - return [obj] - ans = [] - for item in obj: - ans.extend(self.__call__(item)) - for _ in ans: - assert isinstance(_, self.cdatatype) - return ans - - -def pyros_config(): - CONFIG = ConfigDict('PyROS') - - # ================================================ - # === Options common to all solvers - # ================================================ - CONFIG.declare( - 'time_limit', - ConfigValue( - default=None, - domain=NonNegativeFloat, - doc=( - """ - Wall time limit for the execution of the PyROS solver - in seconds (including time spent by subsolvers). - If `None` is provided, then no time limit is enforced. - """ - ), - ), - ) - CONFIG.declare( - 'keepfiles', - ConfigValue( - default=False, - domain=bool, - description=( - """ - Export subproblems with a non-acceptable termination status - for debugging purposes. - If True is provided, then the argument `subproblem_file_directory` - must also be specified. - """ - ), - ), - ) - CONFIG.declare( - 'tee', - ConfigValue( - default=False, - domain=bool, - description="Output subordinate solver logs for all subproblems.", - ), - ) - CONFIG.declare( - 'load_solution', - ConfigValue( - default=True, - domain=bool, - description=( - """ - Load final solution(s) found by PyROS to the deterministic model - provided. - """ - ), - ), - ) - - # ================================================ - # === Required User Inputs - # ================================================ - CONFIG.declare( - "first_stage_variables", - ConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="First-stage (or design) variables.", - visibility=1, - ), - ) - CONFIG.declare( - "second_stage_variables", - ConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="Second-stage (or control) variables.", - visibility=1, - ), - ) - CONFIG.declare( - "uncertain_params", - ConfigValue( - default=[], - domain=InputDataStandardizer(Param, _ParamData), - description=( - """ - Uncertain model parameters. - The `mutable` attribute for all uncertain parameter - objects should be set to True. - """ - ), - visibility=1, - ), - ) - CONFIG.declare( - "uncertainty_set", - ConfigValue( - default=None, - domain=uncertainty_sets, - description=( - """ - Uncertainty set against which the - final solution(s) returned by PyROS should be certified - to be robust. - """ - ), - visibility=1, - ), - ) - CONFIG.declare( - "local_solver", - ConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate local NLP solver.", - visibility=1, - ), - ) - CONFIG.declare( - "global_solver", - ConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate global NLP solver.", - visibility=1, - ), - ) - # ================================================ - # === Optional User Inputs - # ================================================ - CONFIG.declare( - "objective_focus", - ConfigValue( - default=ObjectiveType.nominal, - domain=ValidEnum(ObjectiveType), - description=( - """ - Choice of objective focus to optimize in the master problems. - Choices are: `ObjectiveType.worst_case`, - `ObjectiveType.nominal`. - """ - ), - doc=( - """ - Objective focus for the master problems: - - - `ObjectiveType.nominal`: - Optimize the objective function subject to the nominal - uncertain parameter realization. - - `ObjectiveType.worst_case`: - Optimize the objective function subject to the worst-case - uncertain parameter realization. - - By default, `ObjectiveType.nominal` is chosen. - - A worst-case objective focus is required for certification - of robust optimality of the final solution(s) returned - by PyROS. - If a nominal objective focus is chosen, then only robust - feasibility is guaranteed. - """ - ), - ), - ) - CONFIG.declare( - "nominal_uncertain_param_vals", - ConfigValue( - default=[], - domain=list, - doc=( - """ - Nominal uncertain parameter realization. - Entries should be provided in an order consistent with the - entries of the argument `uncertain_params`. - If an empty list is provided, then the values of the `Param` - objects specified through `uncertain_params` are chosen. - """ - ), - ), - ) - CONFIG.declare( - "decision_rule_order", - ConfigValue( - default=0, - domain=In([0, 1, 2]), - description=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - """ - ), - doc=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - - Choices are: - - - 0: static recourse - - 1: affine recourse - - 2: quadratic recourse - """ - ), - ), - ) - CONFIG.declare( - "solve_master_globally", - ConfigValue( - default=False, - domain=bool, - doc=( - """ - True to solve all master problems with the subordinate - global solver, False to solve all master problems with - the subordinate local solver. - Along with a worst-case objective focus - (see argument `objective_focus`), - solving the master problems to global optimality is required - for certification - of robust optimality of the final solution(s) returned - by PyROS. Otherwise, only robust feasibility is guaranteed. - """ - ), - ), - ) - CONFIG.declare( - "max_iter", - ConfigValue( - default=-1, - domain=PositiveIntOrMinusOne, - description=( - """ - Iteration limit. If -1 is provided, then no iteration - limit is enforced. - """ - ), - ), - ) - CONFIG.declare( - "robust_feasibility_tolerance", - ConfigValue( - default=1e-4, - domain=NonNegativeFloat, - description=( - """ - Relative tolerance for assessing maximal inequality - constraint violations during the GRCS separation step. - """ - ), - ), - ) - CONFIG.declare( - "separation_priority_order", - ConfigValue( - default={}, - domain=dict, - doc=( - """ - Mapping from model inequality constraint names - to positive integers specifying the priorities - of their corresponding separation subproblems. - A higher integer value indicates a higher priority. - Constraints not referenced in the `dict` assume - a priority of 0. - Separation subproblems are solved in order of decreasing - priority. - """ - ), - ), - ) - CONFIG.declare( - "progress_logger", - ConfigValue( - default=default_pyros_solver_logger, - domain=a_logger, - doc=( - """ - Logger (or name thereof) used for reporting PyROS solver - progress. If a `str` is specified, then ``progress_logger`` - is cast to ``logging.getLogger(progress_logger)``. - In the default case, `progress_logger` is set to - a :class:`pyomo.contrib.pyros.util.PreformattedLogger` - object of level ``logging.INFO``. - """ - ), - ), - ) - CONFIG.declare( - "backup_local_solvers", - ConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate local NLP optimizers to invoke - in the event the primary local NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - ), - ) - CONFIG.declare( - "backup_global_solvers", - ConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate global NLP optimizers to invoke - in the event the primary global NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - ), - ) - CONFIG.declare( - "subproblem_file_directory", - ConfigValue( - default=None, - domain=str, - description=( - """ - Directory to which to export subproblems not successfully - solved to an acceptable termination condition. - In the event ``keepfiles=True`` is specified, a str or - path-like referring to an existing directory must be - provided. - """ - ), - ), - ) - - # ================================================ - # === Advanced Options - # ================================================ - CONFIG.declare( - "bypass_local_separation", - ConfigValue( - default=False, - domain=bool, - description=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate global - solver(s) only. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer(s) provided - can quickly solve separation subproblems to global optimality. - """ - ), - ), - ) - CONFIG.declare( - "bypass_global_separation", - ConfigValue( - default=False, - domain=bool, - doc=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate local - solver(s) only. - If `True` is chosen, then robustness of the final solution(s) - returned by PyROS is not guaranteed, and a warning will - be issued at termination. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer provided - cannot tractably solve separation subproblems to global - optimality. - """ - ), - ), - ) - CONFIG.declare( - "p_robustness", - ConfigValue( - default={}, - domain=dict, - doc=( - """ - This is an advanced option. - Add p-robustness constraints to all master subproblems. - If an empty dict is provided, then p-robustness constraints - are not added. - Otherwise, the dict must map a `str` of value ``'rho'`` - to a non-negative `float`. PyROS automatically - specifies ``1 + p_robustness['rho']`` - as an upper bound for the ratio of the - objective function value under any PyROS-sampled uncertain - parameter realization to the objective function under - the nominal parameter realization. - """ - ), - ), - ) - - return CONFIG - - @SolverFactory.register( "pyros", doc="Robust optimization (RO) solver implementing " From 49fa433da7c47646919637a11d9affc0047bd15c Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 20:14:36 -0500 Subject: [PATCH 0273/1178] Apply black, PEP8 code --- pyomo/contrib/pyros/config.py | 37 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 1dc1608ab16..bd87dc743f9 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -3,20 +3,9 @@ """ -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - In, - NonNegativeFloat, -) -from pyomo.core.base import ( - Var, - _VarData, -) -from pyomo.core.base.param import ( - Param, - _ParamData, -) +from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.core.base import Var, _VarData +from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ( a_logger, @@ -122,8 +111,8 @@ def pyros_config(): """ Export subproblems with a non-acceptable termination status for debugging purposes. - If True is provided, then the argument `subproblem_file_directory` - must also be specified. + If True is provided, then the argument + `subproblem_file_directory` must also be specified. """ ), ), @@ -143,8 +132,8 @@ def pyros_config(): domain=bool, description=( """ - Load final solution(s) found by PyROS to the deterministic model - provided. + Load final solution(s) found by PyROS to the deterministic + model provided. """ ), ), @@ -246,7 +235,7 @@ def pyros_config(): uncertain parameter realization. By default, `ObjectiveType.nominal` is chosen. - + A worst-case objective focus is required for certification of robust optimality of the final solution(s) returned by PyROS. @@ -279,19 +268,19 @@ def pyros_config(): domain=In([0, 1, 2]), description=( """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage + Order (or degree) of the polynomial decision rule functions + used for approximating the adjustability of the second stage variables with respect to the uncertain parameters. """ ), doc=( """ - Order (or degree) of the polynomial decision rule functions used + Order (or degree) of the polynomial decision rule functions for approximating the adjustability of the second stage variables with respect to the uncertain parameters. - + Choices are: - + - 0: static recourse - 1: affine recourse - 2: quadratic recourse From 1bda0d3c2d38c6423c0c698f87eec265d311794d Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 20:26:07 -0500 Subject: [PATCH 0274/1178] Update documentation of mandatory args --- pyomo/contrib/pyros/pyros.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 962ae79a436..316a5869057 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -273,21 +273,25 @@ def solve( ---------- model: ConcreteModel The deterministic model. - first_stage_variables: list of Var + first_stage_variables: VarData, Var, or iterable of VarData/Var First-stage model variables (or design variables). - second_stage_variables: list of Var + second_stage_variables: VarData, Var, or iterable of VarData/Var Second-stage model variables (or control variables). - uncertain_params: list of Param + uncertain_params: ParamData, Param, or iterable of ParamData/Param Uncertain model parameters. - The `mutable` attribute for every uncertain parameter - objects must be set to True. + The `mutable` attribute for all uncertain parameter objects + must be set to True. uncertainty_set: UncertaintySet Uncertainty set against which the solution(s) returned will be confirmed to be robust. - local_solver: Solver + local_solver: str or solver type Subordinate local NLP solver. - global_solver: Solver + If a `str` is passed, then the `str` is cast to + ``SolverFactory(local_solver)``. + global_solver: str or solver type Subordinate global NLP solver. + If a `str` is passed, then the `str` is cast to + ``SolverFactory(global_solver)``. Returns ------- From ecc2df8d91bc6e98cb612be631ca197e79adecf4 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 22:19:59 -0500 Subject: [PATCH 0275/1178] Add more rigorous `InputDataStandardizer` checks --- pyomo/contrib/pyros/config.py | 177 +++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index bd87dc743f9..c118b650208 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -3,6 +3,9 @@ """ +from collections.abc import Iterable + +from pyomo.common.collections import ComponentSet from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData @@ -64,23 +67,166 @@ def __call__(self, obj): ) +def mutable_param_validator(param_obj): + """ + Check that Param-like object has attribute `mutable=True`. + + Parameters + ---------- + param_obj : Param or _ParamData + Param-like object of interest. + + Raises + ------ + ValueError + If lengths of the param object and the accompanying + index set do not match. This may occur if some entry + of the Param is not initialized. + ValueError + If attribute `mutable` is of value False. + """ + if len(param_obj) != len(param_obj.index_set()): + raise ValueError( + f"Length of Param component object with " + f"name {param_obj.name!r} is {len(param_obj)}, " + "and does not match that of its index set, " + f"which is of length {len(param_obj.index_set())}. " + "Check that all entries of the component object " + "have been initialized." + ) + if not param_obj.mutable: + raise ValueError( + f"Param object with name {param_obj.name!r} is immutable." + ) + + class InputDataStandardizer(object): - def __init__(self, ctype, cdatatype): + """ + Standardizer for objects castable to a list of Pyomo + component types. + + Parameters + ---------- + ctype : type + Pyomo component type, such as Component, Var or Param. + cdatatype : type + Corresponding Pyomo component data type, such as + _ComponentData, _VarData, or _ParamData. + ctype_validator : callable, optional + Validator function for objects of type `ctype`. + cdatatype_validator : callable, optional + Validator function for objects of type `cdatatype`. + allow_repeats : bool, optional + True to allow duplicate component data entries in final + list to which argument is cast, False otherwise. + + Attributes + ---------- + ctype + cdatatype + ctype_validator + cdatatype_validator + allow_repeats + """ + + def __init__( + self, + ctype, + cdatatype, + ctype_validator=None, + cdatatype_validator=None, + allow_repeats=False, + ): + """Initialize self (see class docstring).""" self.ctype = ctype self.cdatatype = cdatatype + self.ctype_validator = ctype_validator + self.cdatatype_validator = cdatatype_validator + self.allow_repeats = allow_repeats + + def standardize_ctype_obj(self, obj): + """ + Standardize object of type ``self.ctype`` to list + of objects of type ``self.cdatatype``. + """ + if self.ctype_validator is not None: + self.ctype_validator(obj) + return list(obj.values()) + + def standardize_cdatatype_obj(self, obj): + """ + Standarize object of type ``self.cdatatype`` to + ``[obj]``. + """ + if self.cdatatype_validator is not None: + self.cdatatype_validator(obj) + return [obj] + + def __call__(self, obj, from_iterable=None, allow_repeats=None): + """ + Cast object to a flat list of Pyomo component data type + entries. + + Parameters + ---------- + obj : object + Object to be cast. + from_iterable : Iterable or None, optional + Iterable from which `obj` obtained, if any. + allow_repeats : bool or None, optional + True if list can contain repeated entries, + False otherwise. + + Raises + ------ + TypeError + If all entries in the resulting list + are not of type ``self.cdatatype``. + ValueError + If the resulting list contains duplicate entries. + """ + if allow_repeats is None: + allow_repeats = self.allow_repeats - def __call__(self, obj): if isinstance(obj, self.ctype): - return list(obj.values()) - if isinstance(obj, self.cdatatype): - return [obj] - ans = [] - for item in obj: - ans.extend(self.__call__(item)) - for _ in ans: - assert isinstance(_, self.cdatatype) + ans = self.standardize_ctype_obj(obj) + elif isinstance(obj, self.cdatatype): + ans = self.standardize_cdatatype_obj(obj) + elif isinstance(obj, Iterable) and not isinstance(obj, str): + ans = [] + for item in obj: + ans.extend(self.__call__(item, from_iterable=obj)) + else: + from_iterable_qual = ( + f" (entry of iterable {from_iterable})" + if from_iterable is not None + else "" + ) + raise TypeError( + f"Input object {obj!r}{from_iterable_qual} " + "is not of valid component type " + f"{self.ctype.__name__} or component data type " + f"{self.cdatatype.__name__}." + ) + + # check for duplicates if desired + if not allow_repeats and len(ans) != len(ComponentSet(ans)): + comp_name_list = [comp.name for comp in ans] + raise ValueError( + f"Standardized component list {comp_name_list} " + f"derived from input {obj} " + "contains duplicate entries." + ) + return ans + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return ( + f"{self.cdatatype.__name__}, {self.ctype.__name__}, " + f"or Iterable of {self.cdatatype.__name__}/{self.ctype.__name__}" + ) + def pyros_config(): CONFIG = ConfigDict('PyROS') @@ -146,7 +292,7 @@ def pyros_config(): "first_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData), + domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), description="First-stage (or design) variables.", visibility=1, ), @@ -155,7 +301,7 @@ def pyros_config(): "second_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData), + domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), description="Second-stage (or control) variables.", visibility=1, ), @@ -164,7 +310,12 @@ def pyros_config(): "uncertain_params", ConfigValue( default=[], - domain=InputDataStandardizer(Param, _ParamData), + domain=InputDataStandardizer( + ctype=Param, + cdatatype=_ParamData, + ctype_validator=mutable_param_validator, + allow_repeats=False, + ), description=( """ Uncertain model parameters. From bcf1730352471390d81fcd53d9e39d4e758d5336 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 22:21:07 -0500 Subject: [PATCH 0276/1178] Add tests for `InputDataStandardizer` --- pyomo/contrib/pyros/tests/test_config.py | 267 +++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 pyomo/contrib/pyros/tests/test_config.py diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py new file mode 100644 index 00000000000..d0c378a52f0 --- /dev/null +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -0,0 +1,267 @@ +""" +Test objects for construction of PyROS ConfigDict. +""" + + +import unittest + +from pyomo.core.base import ( + ConcreteModel, + Var, + _VarData, +) +from pyomo.core.base.param import Param, _ParamData +from pyomo.contrib.pyros.config import ( + InputDataStandardizer, + mutable_param_validator, +) + + +class testInputDataStandardizer(unittest.TestCase): + """ + Test standardizer method for Pyomo component-type inputs. + """ + + def test_single_component_data(self): + """ + Test standardizer works for single component + data-type entry. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = mdl.v[0] + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 1, + msg="Length of standardizer output is not as expected.", + ) + self.assertIs( + standardizer_output[0], + mdl.v[0], + msg=( + f"Entry {standardizer_output[0]} (id {id(standardizer_output[0])}) " + "is not identical to " + f"input component data object {mdl.v[0]} " + f"(id {id(mdl.v[0])})" + ), + ) + + def test_standardizer_indexed_component(self): + """ + Test component standardizer works on indexed component. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = mdl.v + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 2, + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(standardizer_input.values(), standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_multiple_components(self): + """ + Test standardizer works on sequence of components. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = [mdl.v[0], mdl.x] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.v[0], mdl.x["a"], mdl.x["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_invalid_duplicates(self): + """ + Test standardizer raises exception if input contains duplicates + and duplicates are not allowed. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, _VarData, allow_repeats=False) + + exc_str = r"Standardized.*list.*contains duplicate entries\." + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func([mdl.x, mdl.v, mdl.x]) + + def test_standardizer_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is of invalid type. + """ + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func(2) + + def test_standardizer_iterable_with_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is an iterable with entries of invalid type. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*entry of iterable.*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func([mdl.v, 2]) + + def test_standardizer_invalid_str_passed(self): + """ + Test standardizer raises exception as expected + when input is of invalid type str. + """ + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func("abcd") + + def test_standardizer_invalid_unintialized_params(self): + """ + Test standardizer raises exception when Param with + uninitialized entries passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1]) + + exc_str = r"Length of .*does not match that of.*index set" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_invalid_immutable_params(self): + """ + Test standardizer raises exception when immutable + Param object(s) passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1], initialize=1) + + exc_str = r"Param object with name .*immutable" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_valid_mutable_params(self): + """ + Test Param-like standardizer works as expected for sequence + of valid mutable Param objects. + """ + mdl = ConcreteModel() + mdl.p1 = Param([0, 1], initialize=0, mutable=True) + mdl.p2 = Param(["a", "b"], initialize=1, mutable=True) + + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + standardizer_input = [mdl.p1[0], mdl.p2] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.p1[0], mdl.p2["a"], mdl.p2["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + +if __name__ == "__main__": + unittest.main() From fc497bc6de62734f7d53895062c0fbcc014b944a Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 22:43:43 -0500 Subject: [PATCH 0277/1178] Refactor uncertainty set argument validation --- pyomo/contrib/pyros/config.py | 4 +-- pyomo/contrib/pyros/tests/test_config.py | 28 +++++++++++++++ pyomo/contrib/pyros/uncertainty_sets.py | 45 ++++++++++++++++++++---- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c118b650208..632a226b47b 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -16,7 +16,7 @@ setup_pyros_logger, ValidEnum, ) -from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets +from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain default_pyros_solver_logger = setup_pyros_logger() @@ -330,7 +330,7 @@ def pyros_config(): "uncertainty_set", ConfigValue( default=None, - domain=uncertainty_sets, + domain=UncertaintySetDomain(), description=( """ Uncertainty set against which the diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index d0c378a52f0..6d6c3caf76b 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -14,7 +14,9 @@ from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, + UncertaintySetDomain, ) +from pyomo.contrib.pyros.uncertainty_sets import BoxSet class testInputDataStandardizer(unittest.TestCase): @@ -263,5 +265,31 @@ def test_standardizer_valid_mutable_params(self): ) +class TestUncertaintySetDomain(unittest.TestCase): + """ + Test domain validator for uncertainty set arguments. + """ + def test_uncertainty_set_domain_valid_set(self): + """ + Test validator works for valid argument. + """ + standardizer_func = UncertaintySetDomain() + bset = BoxSet([[0, 1]]) + self.assertIs( + bset, + standardizer_func(bset), + msg="Output of uncertainty set domain not as expected.", + ) + + def test_uncertainty_set_domain_invalid_type(self): + """ + Test validator works for valid argument. + """ + standardizer_func = UncertaintySetDomain() + exc_str = "Expected an .*UncertaintySet object.*received object 2" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(2) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1b51e41fcaf..4a2f198bc17 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -272,12 +272,45 @@ def generate_shape_str(shape, required_shape): ) -def uncertainty_sets(obj): - if not isinstance(obj, UncertaintySet): - raise ValueError( - "Expected an UncertaintySet object, instead received %s" % (obj,) - ) - return obj +class UncertaintySetDomain: + """ + Domain validator for uncertainty set argument. + """ + def __call__(self, obj): + """ + Type validate uncertainty set object. + + Parameters + ---------- + obj : object + Object to validate. + + Returns + ------- + obj : object + Object that was passed, provided type validation successful. + + Raises + ------ + ValueError + If type validation failed. + """ + if not isinstance(obj, UncertaintySet): + raise ValueError( + f"Expected an {UncertaintySet.__name__} object, " + f"instead received object {obj}" + ) + return obj + + def domain_name(self): + """ + Domain name of self. + """ + return UncertaintySet.__name__ + + +# maintain compatibility with prior versions +uncertainty_sets = UncertaintySetDomain() def column(matrix, i): From 497628586141007f142b4a8aa840d57662cc8e94 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:13:22 -0500 Subject: [PATCH 0278/1178] Add more rigorous checks for solver-like args --- pyomo/contrib/pyros/config.py | 282 +++++++++++++++++++++-- pyomo/contrib/pyros/tests/test_config.py | 222 ++++++++++++++++++ pyomo/contrib/pyros/tests/test_grcs.py | 89 ++++++- 3 files changed, 567 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 632a226b47b..b31e404f2f6 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -7,6 +7,7 @@ from pyomo.common.collections import ComponentSet from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory @@ -46,27 +47,6 @@ def PositiveIntOrMinusOne(obj): return ans -class SolverResolvable(object): - def __call__(self, obj): - ''' - if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return a copy of the Solver - if obj is a list, and each element of list is solver resolvable, - return list of solvers - ''' - if isinstance(obj, str): - return SolverFactory(obj.lower()) - elif callable(getattr(obj, "solve", None)): - return obj - elif isinstance(obj, list): - return [self(o) for o in obj] - else: - raise ValueError( - "Expected a Pyomo solver or string object, " - "instead received {0}".format(obj.__class__.__name__) - ) - - def mutable_param_validator(param_obj): """ Check that Param-like object has attribute `mutable=True`. @@ -228,6 +208,247 @@ def domain_name(self): ) +class NotSolverResolvable(Exception): + """ + Exception type for failure to cast an object to a Pyomo solver. + """ + + +class SolverResolvable(object): + """ + Callable for casting an object (such as a str) + to a Pyomo solver. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'local solver' + or 'global solver'. This argument is used + for constructing error/exception messages. + + Attributes + ---------- + require_available + solver_desc + """ + + def __init__(self, require_available=True, solver_desc="solver"): + """Initialize self (see class docstring).""" + self.require_available = require_available + self.solver_desc = solver_desc + + @staticmethod + def is_solver_type(obj): + """ + Return True if object is considered a Pyomo solver, + False otherwise. + + An object is considered a Pyomo solver provided that + it has callable attributes named 'solve' and + 'available'. + """ + return callable(getattr(obj, "solve", None)) and callable( + getattr(obj, "available", None) + ) + + def __call__(self, obj, require_available=None, solver_desc=None): + """ + Cast object to a Pyomo solver. + + If `obj` is a string, then ``SolverFactory(obj.lower())`` + is returned. If `obj` is a Pyomo solver type, then + `obj` is returned. + + Parameters + ---------- + obj : object + Object to be cast to Pyomo solver type. + require_available : bool or None, optional + True if `available()` method of the resolved solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Brief description of the solver, such as 'local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + Solver + Pyomo solver. + + Raises + ------ + NotSolverResolvable + If `obj` cannot be cast to a Pyomo solver because + it is neither a str nor a Pyomo solver type. + ApplicationError + In event that solver is not available, the + method `available(exception_flag=True)` of the + solver to which `obj` is cast should raise an + exception of this type. The present method + will also emit a more detailed error message + through the default PyROS logger. + """ + # resort to defaults if necessary + if require_available is None: + require_available = self.require_available + if solver_desc is None: + solver_desc = self.solver_desc + + # perform casting + if isinstance(obj, str): + solver = SolverFactory(obj.lower()) + elif self.is_solver_type(obj): + solver = obj + else: + raise NotSolverResolvable( + f"Cannot cast object `{obj!r}` to a Pyomo optimizer for use as " + f"{solver_desc}, as the object is neither a str nor a " + f"Pyomo Solver type (got type {type(obj).__name__})." + ) + + # availability check, if so desired + if require_available: + try: + solver.available(exception_flag=True) + except ApplicationError: + default_pyros_solver_logger.exception( + f"Output of `available()` method for {solver_desc} " + f"with repr {solver!r} resolved from object {obj} " + "is not `True`. " + "Check solver and any required dependencies " + "have been set up properly." + ) + raise + + return solver + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str or Solver" + + +class SolverIterable(object): + """ + Callable for casting an iterable (such as a list of strs) + to a list of Pyomo solvers. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + filter_by_availability : bool, optional + True to remove standardized solvers for which `available()` + does not return True, False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'backup local solver' + or 'backup global solver'. + """ + + def __init__( + self, + require_available=True, + filter_by_availability=True, + solver_desc="solver", + ): + """Initialize self (see class docstring). + + """ + self.require_available = require_available + self.filter_by_availability = filter_by_availability + self.solver_desc = solver_desc + + def __call__( + self, + obj, + require_available=None, + filter_by_availability=None, + solver_desc=None, + ): + """ + Cast iterable object to a list of Pyomo solver objects. + + Parameters + ---------- + obj : str, Solver, or Iterable of str/Solver + Object of interest. + require_available : bool or None, optional + True if `available()` method of each solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Descriptor for the solver, such as 'backup local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + solvers : list of solver type + List of solver objects to which obj is cast. + + Raises + ------ + TypeError + If `obj` is a str. + """ + if require_available is None: + require_available = self.require_available + if filter_by_availability is None: + filter_by_availability = self.filter_by_availability + if solver_desc is None: + solver_desc = self.solver_desc + + solver_resolve_func = SolverResolvable() + + if isinstance(obj, str) or solver_resolve_func.is_solver_type(obj): + # single solver resolvable is cast to singleton list. + # perform explicit check for str, otherwise this method + # would attempt to resolve each character. + obj_as_list = [obj] + else: + obj_as_list = list(obj) + + solvers = [] + for idx, val in enumerate(obj_as_list): + solver_desc_str = f"{solver_desc} " f"(index {idx})" + opt = solver_resolve_func( + obj=val, + require_available=require_available, + solver_desc=solver_desc_str, + ) + if filter_by_availability and not opt.available(exception_flag=False): + default_pyros_solver_logger.warning( + f"Output of `available()` method for solver object {opt} " + f"resolved from object {val} of sequence {obj_as_list} " + f"to be used as {self.solver_desc} " + "is not `True`. " + "Removing from list of standardized solvers." + ) + else: + solvers.append(opt) + + return solvers + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str, solver type, or Iterable of str/solver type" + + def pyros_config(): CONFIG = ConfigDict('PyROS') @@ -345,7 +566,7 @@ def pyros_config(): "local_solver", ConfigValue( default=None, - domain=SolverResolvable(), + domain=SolverResolvable(solver_desc="local solver", require_available=True), description="Subordinate local NLP solver.", visibility=1, ), @@ -354,7 +575,10 @@ def pyros_config(): "global_solver", ConfigValue( default=None, - domain=SolverResolvable(), + domain=SolverResolvable( + solver_desc="global solver", + require_available=True, + ), description="Subordinate global NLP solver.", visibility=1, ), @@ -525,7 +749,11 @@ def pyros_config(): "backup_local_solvers", ConfigValue( default=[], - domain=SolverResolvable(), + domain=SolverIterable( + solver_desc="backup local solver", + require_available=False, + filter_by_availability=True, + ), doc=( """ Additional subordinate local NLP optimizers to invoke @@ -539,7 +767,11 @@ def pyros_config(): "backup_global_solvers", ConfigValue( default=[], - domain=SolverResolvable(), + domain=SolverIterable( + solver_desc="backup global solver", + require_available=False, + filter_by_availability=True, + ), doc=( """ Additional subordinate global NLP optimizers to invoke diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 6d6c3caf76b..05ea35c3dda 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -3,6 +3,7 @@ """ +import logging import unittest from pyomo.core.base import ( @@ -10,12 +11,18 @@ Var, _VarData, ) +from pyomo.common.log import LoggingIntercept +from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, _ParamData from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, + NotSolverResolvable, + SolverIterable, + SolverResolvable, UncertaintySetDomain, ) +from pyomo.opt import SolverFactory, SolverResults from pyomo.contrib.pyros.uncertainty_sets import BoxSet @@ -291,5 +298,220 @@ def test_uncertainty_set_domain_invalid_type(self): standardizer_func(2) +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestSolverResolvable(unittest.TestCase): + """ + Test PyROS standardizer for solver-type objects. + """ + + def test_solver_resolvable_valid_str(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver_str = "ipopt" + standardizer_func = SolverResolvable() + solver = standardizer_func(solver_str) + expected_solver_type = type(SolverFactory(solver_str)) + + self.assertIsInstance( + solver, + type(SolverFactory(solver_str)), + msg=( + "SolverResolvable object should be of type " + f"{expected_solver_type.__name__}, " + f"but got object of type {solver.__class__.__name__}." + ), + ) + + def test_solver_resolvable_valid_solver_type(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver = SolverFactory("ipopt") + standardizer_func = SolverResolvable() + standardized_solver = standardizer_func(solver) + + self.assertIs( + solver, + standardized_solver, + msg=( + f"Test solver {solver} and standardized solver " + f"{standardized_solver} are not identical." + ), + ) + + def test_solver_resolvable_invalid_type(self): + """ + Test solver resolvable object raises expected + exception when invalid entry is provided. + """ + invalid_object = 2 + standardizer_func = SolverResolvable(solver_desc="local solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"local solver.*got type int.*" + ) + with self.assertRaisesRegex(NotSolverResolvable, exc_str): + standardizer_func(invalid_object) + + def test_solver_resolvable_unavailable_solver(self): + """ + Test solver standardizer fails in event solver is + unavaiable. + """ + unavailable_solver = UnavailableSolver() + standardizer_func = SolverResolvable( + solver_desc="local solver", require_available=True + ) + + exc_str = r"Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + with LoggingIntercept(level=logging.ERROR) as LOG: + standardizer_func(unavailable_solver) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*local solver.*" + ) + + +class TestSolverIterable(unittest.TestCase): + """ + Test standardizer method for iterable of solvers, + used to validate `backup_local_solvers` and `backup_global_solvers` + arguments. + """ + + def test_solver_iterable_valid_list(self): + """ + Test solver type standardizer works for list of valid + objects castable to solver. + """ + solver_list = ["ipopt", SolverFactory("ipopt")] + expected_solver_types = [type(SolverFactory("ipopt"))] * 2 + standardizer_func = SolverIterable() + + standardized_solver_list = standardizer_func(solver_list) + + # check list of solver types returned + for idx, standardized_solver in enumerate(standardized_solver_list): + self.assertIsInstance( + standardized_solver, + expected_solver_types[idx], + msg=( + f"Standardized solver {standardized_solver} " + f"(index {idx}) expected to be of type " + f"{expected_solver_types[idx].__name__}, " + f"but is of type {standardized_solver.__class__.__name__}" + ), + ) + + # second entry of standardized solver list should be the same + # object as that of input list, since the input solver is a Pyomo + # solver type + self.assertIs( + standardized_solver_list[1], + solver_list[1], + msg=( + f"Test solver {solver_list[1]} and standardized solver " + f"{standardized_solver_list[1]} should be identical." + ), + ) + + def test_solver_iterable_valid_str(self): + """ + Test SolverIterable raises exception when str passed. + """ + solver_str = "ipopt" + standardizer_func = SolverIterable() + + solver_list = standardizer_func(solver_str) + self.assertEqual( + len(solver_list), 1, "Standardized solver list is not of expected length" + ) + + def test_solver_iterable_unavailable_solver(self): + """ + Test SolverIterable addresses unavailable solvers appropriately. + """ + solvers = (SolverFactory("ipopt"), UnavailableSolver()) + + standardizer_func = SolverIterable( + require_available=True, + filter_by_availability=True, + solver_desc="example solver list", + ) + exc_str = r"Solver.*UnavailableSolver.* not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers) + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers, filter_by_availability=False) + + standardized_solver_list = standardizer_func( + solvers, + filter_by_availability=True, + require_available=False, + ) + self.assertEqual( + len(standardized_solver_list), + 1, + msg=( + "Length of filtered standardized solver list not as " + "expected." + ), + ) + self.assertIs( + standardized_solver_list[0], + solvers[0], + msg="Entry of filtered standardized solver list not as expected.", + ) + + standardized_solver_list = standardizer_func( + solvers, + filter_by_availability=False, + require_available=False, + ) + self.assertEqual( + len(standardized_solver_list), + 2, + msg=( + "Length of filtered standardized solver list not as " + "expected." + ), + ) + self.assertEqual( + standardized_solver_list, + list(solvers), + msg="Entry of filtered standardized solver list not as expected.", + ) + + def test_solver_iterable_invalid_list(self): + """ + Test SolverIterable raises exception if iterable contains + at least one invalid object. + """ + invalid_object = ["ipopt", 2] + standardizer_func = SolverIterable(solver_desc="backup solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"backup solver.*index 1.*got type int.*" + ) + with self.assertRaisesRegex(NotSolverResolvable, exc_str): + standardizer_func(invalid_object) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 8de1c2666b9..a05e5f06134 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -137,7 +137,7 @@ def __init__(self, calls_to_sleep, max_time, sub_solver): self.num_calls = 0 self.options = Bunch() - def available(self): + def available(self, exception_flag=True): return True def license_is_valid(self): @@ -6302,5 +6302,92 @@ def test_log_disclaimer(self): ) +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestPyROSUnavailableSubsolvers(unittest.TestCase): + """ + Check that appropriate exceptionsa are raised if + PyROS is invoked with unavailable subsolvers. + """ + + def test_pyros_unavailable_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + exc_str = r".*Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ValueError, exc_str): + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.ERROR) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SolverFactory("ipopt"), + global_solver=UnavailableSolver(), + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*global solver.*" + ) + + def test_pyros_unavailable_backup_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable backup subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.WARNING) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SolverFactory("ipopt"), + global_solver=SolverFactory("ipopt"), + backup_global_solvers=[UnavailableSolver()], + bypass_global_separation=True, + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, + r"Output of `available\(\)` method.*backup global solver.*" + r"Removing from list.*" + ) + + if __name__ == "__main__": unittest.main() From 9e89fee1d9f2984b664fcc68715843410a9feb21 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:38:23 -0500 Subject: [PATCH 0279/1178] Extend domain of objective focus argument --- pyomo/contrib/pyros/config.py | 5 ++-- pyomo/contrib/pyros/tests/test_config.py | 37 ++++++++++++++++++++++++ pyomo/contrib/pyros/util.py | 18 ------------ 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index b31e404f2f6..5c51ab546db 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -6,7 +6,7 @@ from collections.abc import Iterable from pyomo.common.collections import ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData @@ -15,7 +15,6 @@ a_logger, ObjectiveType, setup_pyros_logger, - ValidEnum, ) from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain @@ -590,7 +589,7 @@ def pyros_config(): "objective_focus", ConfigValue( default=ObjectiveType.nominal, - domain=ValidEnum(ObjectiveType), + domain=InEnum(ObjectiveType), description=( """ Choice of objective focus to optimize in the master problems. diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 05ea35c3dda..727c5443315 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -21,7 +21,9 @@ SolverIterable, SolverResolvable, UncertaintySetDomain, + pyros_config, ) +from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults from pyomo.contrib.pyros.uncertainty_sets import BoxSet @@ -513,5 +515,40 @@ def test_solver_iterable_invalid_list(self): standardizer_func(invalid_object) +class TestPyROSConfig(unittest.TestCase): + """ + Test PyROS ConfigDict behaves as expected. + """ + + CONFIG = pyros_config() + + def test_config_objective_focus(self): + """ + Test config parses objective focus as expected. + """ + config = self.CONFIG() + + for obj_focus_name in ["nominal", "worst_case"]: + config.objective_focus = obj_focus_name + self.assertEqual( + config.objective_focus, + ObjectiveType[obj_focus_name], + msg="Objective focus not set as expected." + ) + + for obj_focus in ObjectiveType: + config.objective_focus = obj_focus + self.assertEqual( + config.objective_focus, + obj_focus, + msg="Objective focus not set as expected." + ) + + invalid_focus = "test_example" + exc_str = f".*{invalid_focus!r} is not a valid ObjectiveType" + with self.assertRaisesRegex(ValueError, exc_str): + config.objective_focus = invalid_focus + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index e2986ae18c7..e67d55dfb68 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -461,24 +461,6 @@ def a_logger(str_or_logger): return logging.getLogger(str_or_logger) -def ValidEnum(enum_class): - ''' - Python 3 dependent format string - ''' - - def fcn(obj): - if obj not in enum_class: - raise ValueError( - "Expected an {0} object, " - "instead received {1}".format( - enum_class.__name__, obj.__class__.__name__ - ) - ) - return obj - - return fcn - - class pyrosTerminationCondition(Enum): """Enumeration of all possible PyROS termination conditions.""" From 039171f13bcbc6efc913466d67db15c1c251156d Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:47:27 -0500 Subject: [PATCH 0280/1178] Extend domain for path-like args --- pyomo/contrib/pyros/config.py | 56 ++++++++++++++- pyomo/contrib/pyros/tests/test_config.py | 92 +++++++++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 5c51ab546db..90d8e8cfb3e 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -4,9 +4,10 @@ from collections.abc import Iterable +import os from pyomo.common.collections import ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum +from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum, Path from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData @@ -46,6 +47,57 @@ def PositiveIntOrMinusOne(obj): return ans +class PathLikeOrNone: + """ + Validator for path-like objects. + + This interface is a wrapper around the domain validator + ``common.config.Path``, and extends the domain of interest to + to include: + - None + - objects following the Python ``os.PathLike`` protocol. + + Parameters + ---------- + **config_path_kwargs : dict + Keyword arguments to ``common.config.Path``. + """ + + def __init__(self, **config_path_kwargs): + """Initialize self (see class docstring).""" + self.config_path = Path(**config_path_kwargs) + + def __call__(self, path): + """ + Cast path to expanded string representation. + + Parameters + ---------- + path : None str, bytes, or path-like + Object to be cast. + + Returns + ------- + None + If obj is None. + str + String representation of path-like object. + """ + if path is None: + return path + + # prevent common.config.Path from invoking + # str() on the path-like object + path_str = os.fsdecode(path) + + # standardize path str as necessary + return self.config_path(path_str) + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "path-like or None" + + def mutable_param_validator(param_obj): """ Check that Param-like object has attribute `mutable=True`. @@ -784,7 +836,7 @@ def pyros_config(): "subproblem_file_directory", ConfigValue( default=None, - domain=str, + domain=PathLikeOrNone(), description=( """ Directory to which to export subproblems not successfully diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 727c5443315..2e957cc7df6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -4,6 +4,7 @@ import logging +import os import unittest from pyomo.core.base import ( @@ -11,6 +12,7 @@ Var, _VarData, ) +from pyomo.common.config import Path from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, _ParamData @@ -18,10 +20,11 @@ InputDataStandardizer, mutable_param_validator, NotSolverResolvable, + PathLikeOrNone, + pyros_config, SolverIterable, SolverResolvable, UncertaintySetDomain, - pyros_config, ) from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults @@ -550,5 +553,92 @@ def test_config_objective_focus(self): config.objective_focus = invalid_focus +class testPathLikeOrNone(unittest.TestCase): + """ + Test interface for validating path-like arguments. + """ + + def test_none_valid(self): + """ + Test `None` is valid. + """ + standardizer_func = PathLikeOrNone() + + self.assertIs( + standardizer_func(None), + None, + msg="Output of `PathLikeOrNone` standardizer not as expected.", + ) + + def test_str_bytes_path_like_valid(self): + """ + Check path-like validator handles str, bytes, and path-like + inputs correctly. + """ + + class ExamplePathLike(os.PathLike): + """ + Path-like class for testing. Key feature: __fspath__ + and __str__ return different outputs. + """ + + def __init__(self, path_str_or_bytes): + self.path = path_str_or_bytes + + def __fspath__(self): + return self.path + + def __str__(self): + path_str = os.fsdecode(self.path) + return f"{type(self).__name__}({path_str})" + + path_standardization_func = PathLikeOrNone() + + # construct path arguments of different type + path_as_str = "example_output_dir/" + path_as_bytes = os.fsencode(path_as_str) + path_like_from_str = ExamplePathLike(path_as_str) + path_like_from_bytes = ExamplePathLike(path_as_bytes) + + # for all possible arguments, output should be + # the str returned by ``common.config.Path`` when + # string representation of the path is input. + expected_output = Path()(path_as_str) + + # check output is as expected in all cases + self.assertEqual( + path_standardization_func(path_as_str), + expected_output, + msg=( + "Path-like validator output from str input " + "does not match expected value." + ), + ) + self.assertEqual( + path_standardization_func(path_as_bytes), + expected_output, + msg=( + "Path-like validator output from bytes input " + "does not match expected value." + ), + ) + self.assertEqual( + path_standardization_func(path_like_from_str), + expected_output, + msg=( + "Path-like validator output from path-like input " + "derived from str does not match expected value." + ), + ) + self.assertEqual( + path_standardization_func(path_like_from_bytes), + expected_output, + msg=( + "Path-like validator output from path-like input " + "derived from bytes does not match expected value." + ), + ) + + if __name__ == "__main__": unittest.main() From c0f2a41d439e2a45ecc0b6bd853d8216c73f6b02 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:56:15 -0500 Subject: [PATCH 0281/1178] Tweak domain name of path-like args --- pyomo/contrib/pyros/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 90d8e8cfb3e..a6e387b62e9 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -95,7 +95,7 @@ def __call__(self, path): def domain_name(self): """Return str briefly describing domain encompassed by self.""" - return "path-like or None" + return "str, bytes, path-like or None" def mutable_param_validator(param_obj): From 154bbba87730f62d4e270f3bd80161f51c258e46 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:59:23 -0500 Subject: [PATCH 0282/1178] Apply black --- pyomo/contrib/pyros/config.py | 41 +++++++++--------------- pyomo/contrib/pyros/tests/test_config.py | 29 +++++------------ pyomo/contrib/pyros/tests/test_grcs.py | 8 ++--- 3 files changed, 28 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index a6e387b62e9..663e0252032 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -7,16 +7,19 @@ import os from pyomo.common.collections import ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum, Path +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + In, + NonNegativeFloat, + InEnum, + Path, +) from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.util import ( - a_logger, - ObjectiveType, - setup_pyros_logger, -) +from pyomo.contrib.pyros.util import a_logger, ObjectiveType, setup_pyros_logger from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain @@ -126,9 +129,7 @@ def mutable_param_validator(param_obj): "have been initialized." ) if not param_obj.mutable: - raise ValueError( - f"Param object with name {param_obj.name!r} is immutable." - ) + raise ValueError(f"Param object with name {param_obj.name!r} is immutable.") class InputDataStandardizer(object): @@ -409,25 +410,16 @@ class SolverIterable(object): """ def __init__( - self, - require_available=True, - filter_by_availability=True, - solver_desc="solver", - ): - """Initialize self (see class docstring). - - """ + self, require_available=True, filter_by_availability=True, solver_desc="solver" + ): + """Initialize self (see class docstring).""" self.require_available = require_available self.filter_by_availability = filter_by_availability self.solver_desc = solver_desc def __call__( - self, - obj, - require_available=None, - filter_by_availability=None, - solver_desc=None, - ): + self, obj, require_available=None, filter_by_availability=None, solver_desc=None + ): """ Cast iterable object to a list of Pyomo solver objects. @@ -627,8 +619,7 @@ def pyros_config(): ConfigValue( default=None, domain=SolverResolvable( - solver_desc="global solver", - require_available=True, + solver_desc="global solver", require_available=True ), description="Subordinate global NLP solver.", visibility=1, diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 2e957cc7df6..938fbe8b8e1 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -7,11 +7,7 @@ import os import unittest -from pyomo.core.base import ( - ConcreteModel, - Var, - _VarData, -) +from pyomo.core.base import ConcreteModel, Var, _VarData from pyomo.common.config import Path from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError @@ -281,6 +277,7 @@ class TestUncertaintySetDomain(unittest.TestCase): """ Test domain validator for uncertainty set arguments. """ + def test_uncertainty_set_domain_valid_set(self): """ Test validator works for valid argument. @@ -465,17 +462,12 @@ def test_solver_iterable_unavailable_solver(self): standardizer_func(solvers, filter_by_availability=False) standardized_solver_list = standardizer_func( - solvers, - filter_by_availability=True, - require_available=False, + solvers, filter_by_availability=True, require_available=False ) self.assertEqual( len(standardized_solver_list), 1, - msg=( - "Length of filtered standardized solver list not as " - "expected." - ), + msg=("Length of filtered standardized solver list not as " "expected."), ) self.assertIs( standardized_solver_list[0], @@ -484,17 +476,12 @@ def test_solver_iterable_unavailable_solver(self): ) standardized_solver_list = standardizer_func( - solvers, - filter_by_availability=False, - require_available=False, + solvers, filter_by_availability=False, require_available=False ) self.assertEqual( len(standardized_solver_list), 2, - msg=( - "Length of filtered standardized solver list not as " - "expected." - ), + msg=("Length of filtered standardized solver list not as " "expected."), ) self.assertEqual( standardized_solver_list, @@ -536,7 +523,7 @@ def test_config_objective_focus(self): self.assertEqual( config.objective_focus, ObjectiveType[obj_focus_name], - msg="Objective focus not set as expected." + msg="Objective focus not set as expected.", ) for obj_focus in ObjectiveType: @@ -544,7 +531,7 @@ def test_config_objective_focus(self): self.assertEqual( config.objective_focus, obj_focus, - msg="Objective focus not set as expected." + msg="Objective focus not set as expected.", ) invalid_focus = "test_example" diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index a05e5f06134..a75aa4dcf41 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3766,9 +3766,9 @@ def test_solve_master(self): master_data.master_model.scenarios[0, 0].second_stage_objective = Expression( expr=master_data.master_model.scenarios[0, 0].x ) - master_data.master_model.scenarios[0, 0].util.dr_var_to_exponent_map = ( - ComponentMap() - ) + master_data.master_model.scenarios[ + 0, 0 + ].util.dr_var_to_exponent_map = ComponentMap() master_data.iteration = 0 master_data.timing = TimingData() @@ -6385,7 +6385,7 @@ def test_pyros_unavailable_backup_subsolver(self): self.assertRegex( error_msgs, r"Output of `available\(\)` method.*backup global solver.*" - r"Removing from list.*" + r"Removing from list.*", ) From d7b41d5351b57da5ae377b271aff78098de964ea Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 12:03:24 -0500 Subject: [PATCH 0283/1178] Refactor checks for int-like args --- pyomo/contrib/pyros/config.py | 60 +++++++++++++++--------- pyomo/contrib/pyros/tests/test_config.py | 35 ++++++++++++++ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 663e0252032..19fe6c710ef 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -26,28 +26,42 @@ default_pyros_solver_logger = setup_pyros_logger() -def NonNegIntOrMinusOne(obj): - ''' - if obj is a non-negative int, return the non-negative int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans < 0 and ans != -1): - raise ValueError("Expected non-negative int, but received %s" % (obj,)) - return ans - - -def PositiveIntOrMinusOne(obj): - ''' - if obj is a positive int, return the int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError("Expected positive int, but received %s" % (obj,)) - return ans +class PositiveIntOrMinusOne: + """ + Domain validator for objects castable to a + strictly positive int or -1. + """ + + def __call__(self, obj): + """ + Cast object to positive int or -1. + + Parameters + ---------- + obj : object + Object of interest. + + Returns + ------- + int + Positive int, or -1. + + Raises + ------ + ValueError + If object not castable to positive int, or -1. + """ + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError( + "Expected positive int or -1, " + f"but received value {obj!r}" + ) + return ans + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "positive int or -1" class PathLikeOrNone: @@ -729,7 +743,7 @@ def pyros_config(): "max_iter", ConfigValue( default=-1, - domain=PositiveIntOrMinusOne, + domain=PositiveIntOrMinusOne(), description=( """ Iteration limit. If -1 is provided, then no iteration diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 938fbe8b8e1..4417966bf72 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -17,6 +17,7 @@ mutable_param_validator, NotSolverResolvable, PathLikeOrNone, + PositiveIntOrMinusOne, pyros_config, SolverIterable, SolverResolvable, @@ -627,5 +628,39 @@ def __str__(self): ) +class TestPositiveIntOrMinusOne(unittest.TestCase): + """ + Test validator for -1 or positive int works as expected. + """ + + def test_positive_int_or_minus_one(self): + """ + Test positive int or -1 validator works as expected. + """ + standardizer_func = PositiveIntOrMinusOne() + self.assertIs( + standardizer_func(1.0), + 1, + msg=( + f"{PositiveIntOrMinusOne.__name__} " + "does not standardize as expected." + ), + ) + self.assertEqual( + standardizer_func(-1.00), + -1, + msg=( + f"{PositiveIntOrMinusOne.__name__} " + "does not standardize as expected." + ), + ) + + exc_str = r"Expected positive int or -1, but received value.*" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(1.5) + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(0) + + if __name__ == "__main__": unittest.main() From 2a0c7907a11972f5ec7e41a879a7ce6e3b8f18d7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 12:46:33 -0500 Subject: [PATCH 0284/1178] Refactor logger type validator --- pyomo/contrib/pyros/config.py | 40 ++++++++++++++++++++++-- pyomo/contrib/pyros/tests/test_config.py | 28 +++++++++++++++++ pyomo/contrib/pyros/util.py | 27 ---------------- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 19fe6c710ef..8bafb4ea6dd 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -4,6 +4,7 @@ from collections.abc import Iterable +import logging import os from pyomo.common.collections import ComponentSet @@ -19,13 +20,45 @@ from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.util import a_logger, ObjectiveType, setup_pyros_logger +from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain default_pyros_solver_logger = setup_pyros_logger() +class LoggerType: + """ + Domain validator for objects castable to logging.Logger. + """ + + def __call__(self, obj): + """ + Cast object to logger. + + Parameters + ---------- + obj : object + Object to be cast. + + Returns + ------- + logging.Logger + If `str_or_logger` is of type `logging.Logger`,then + `str_or_logger` is returned. + Otherwise, ``logging.getLogger(str_or_logger)`` + is returned. + """ + if isinstance(obj, logging.Logger): + return obj + else: + return logging.getLogger(obj) + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "None, str or logging.Logger" + + class PositiveIntOrMinusOne: """ Domain validator for objects castable to a @@ -788,11 +821,12 @@ def pyros_config(): "progress_logger", ConfigValue( default=default_pyros_solver_logger, - domain=a_logger, + domain=LoggerType(), doc=( """ Logger (or name thereof) used for reporting PyROS solver - progress. If a `str` is specified, then ``progress_logger`` + progress. If `None` or a `str` is provided, then + ``progress_logger`` is cast to ``logging.getLogger(progress_logger)``. In the default case, `progress_logger` is set to a :class:`pyomo.contrib.pyros.util.PreformattedLogger` diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 4417966bf72..73a6678bb9d 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -15,6 +15,7 @@ from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, + LoggerType, NotSolverResolvable, PathLikeOrNone, PositiveIntOrMinusOne, @@ -662,5 +663,32 @@ def test_positive_int_or_minus_one(self): standardizer_func(0) +class TestLoggerType(unittest.TestCase): + """ + Test logger type validator. + """ + + def test_logger_type(self): + """ + Test logger type validator. + """ + standardizer_func = LoggerType() + mylogger = logging.getLogger("example") + self.assertIs( + standardizer_func(mylogger), + mylogger, + msg=f"{LoggerType.__name__} output not as expected", + ) + self.assertIs( + standardizer_func(mylogger.name), + mylogger, + msg=f"{LoggerType.__name__} output not as expected", + ) + + exc_str = r"A logger name must be a string" + with self.assertRaisesRegex(Exception, exc_str): + standardizer_func(2) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index e67d55dfb68..30b5d2df427 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -434,33 +434,6 @@ def setup_pyros_logger(name=DEFAULT_LOGGER_NAME): return logger -def a_logger(str_or_logger): - """ - Standardize a string or logger object to a logger object. - - Parameters - ---------- - str_or_logger : str or logging.Logger - String or logger object to normalize. - - Returns - ------- - logging.Logger - If `str_or_logger` is of type `logging.Logger`,then - `str_or_logger` is returned. - Otherwise, ``logging.getLogger(str_or_logger)`` - is returned. In the event `str_or_logger` is - the name of the default PyROS logger, the logger level - is set to `logging.INFO`, and a `PreformattedLogger` - instance is returned in lieu of a standard `Logger` - instance. - """ - if isinstance(str_or_logger, logging.Logger): - return logging.getLogger(str_or_logger.name) - else: - return logging.getLogger(str_or_logger) - - class pyrosTerminationCondition(Enum): """Enumeration of all possible PyROS termination conditions.""" From 14421fa02711efa171f3e1997d358e88c5b5bf70 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 12:58:39 -0500 Subject: [PATCH 0285/1178] Apply black --- pyomo/contrib/pyros/config.py | 5 +---- pyomo/contrib/pyros/tests/test_config.py | 6 ++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 8bafb4ea6dd..17e4d3804d0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -86,10 +86,7 @@ def __call__(self, obj): """ ans = int(obj) if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError( - "Expected positive int or -1, " - f"but received value {obj!r}" - ) + raise ValueError(f"Expected positive int or -1, but received value {obj!r}") return ans def domain_name(self): diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 73a6678bb9d..821b1fe7d1e 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -643,16 +643,14 @@ def test_positive_int_or_minus_one(self): standardizer_func(1.0), 1, msg=( - f"{PositiveIntOrMinusOne.__name__} " - "does not standardize as expected." + f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." ), ) self.assertEqual( standardizer_func(-1.00), -1, msg=( - f"{PositiveIntOrMinusOne.__name__} " - "does not standardize as expected." + f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." ), ) From be98fa9721c7a7015573671238cc62180f561983 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 14:44:40 -0500 Subject: [PATCH 0286/1178] Restructure PyROS argument resolution and validation --- pyomo/contrib/pyros/config.py | 91 +++++++++++++++ pyomo/contrib/pyros/pyros.py | 71 +++++++++--- pyomo/contrib/pyros/tests/test_config.py | 66 +++++++++++ pyomo/contrib/pyros/tests/test_grcs.py | 140 +++++++++++++++++++++++ 4 files changed, 349 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 17e4d3804d0..c003d699255 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -950,3 +950,94 @@ def pyros_config(): ) return CONFIG + + +def resolve_keyword_arguments(prioritized_kwargs_dicts, func=None): + """ + Resolve the keyword arguments to a callable in the event + the arguments may have been passed in one or more possible + ways. + + A warning-level message is logged (through the default PyROS + logger) in the event an argument is specified in more than one + way. In this case, the value provided through the means with + the highest priority is selected. + + Parameters + ---------- + prioritized_kwargs_dicts : dict + Each entry maps a str to a dict of the keyword arguments + passed via the means described by the str. + Entries of `prioritized_kwargs_dicts` are taken to be + provided in descending order of priority of the means + by which the arguments may have been passed to the callable. + func : callable or None, optional + Callable to which the keyword arguments are/were passed. + Currently, only the `__name__` attribute is used, + for the purpose of logging warning-level messages. + If `None` is passed, then the warning messages + logged are slightly less informative. + + Returns + ------- + resolved_kwargs : dict + Resolved keyword arguments. + """ + # warnings are issued through logger object + default_logger = default_pyros_solver_logger + + # used for warning messages + func_desc = f"passed to {func.__name__}()" if func is not None else "passed" + + # we will loop through the priority dict. initialize: + # - resolved keyword arguments, taking into account the + # priority order and overlap + # - kwarg dicts already processed + # - sequence of kwarg dicts yet to be processed + resolved_kwargs = dict() + prev_prioritized_kwargs_dicts = dict() + remaining_kwargs_dicts = prioritized_kwargs_dicts.copy() + for curr_desc, curr_kwargs in remaining_kwargs_dicts.items(): + overlapping_args = dict() + overlapping_args_set = set() + + for prev_desc, prev_kwargs in prev_prioritized_kwargs_dicts.items(): + # determine overlap between currrent and previous + # set of kwargs, and remove overlap of current + # and higher priority sets from the result + curr_prev_overlapping_args = ( + set(curr_kwargs.keys()) & set(prev_kwargs.keys()) + ) - overlapping_args_set + if curr_prev_overlapping_args: + # if there is overlap, prepare overlapping args + # for when warning is to be issued + overlapping_args[prev_desc] = curr_prev_overlapping_args + + # update set of args overlapping with higher priority dicts + overlapping_args_set |= curr_prev_overlapping_args + + # ensure kwargs specified in higher priority + # dicts are not overwritten in resolved kwargs + resolved_kwargs.update( + { + kw: val + for kw, val in curr_kwargs.items() + if kw not in overlapping_args_set + } + ) + + # if there are overlaps, log warnings accordingly + # per priority level + for overlap_desc, args_set in overlapping_args.items(): + new_overlapping_args_str = ", ".join(f"{arg!r}" for arg in args_set) + default_logger.warning( + f"Arguments [{new_overlapping_args_str}] passed {curr_desc} " + f"already {func_desc} {overlap_desc}, " + "and will not be overwritten. " + "Consider modifying your arguments to remove the overlap." + ) + + # increment sequence of kwarg dicts already processed + prev_prioritized_kwargs_dicts[curr_desc] = curr_kwargs + + return resolved_kwargs diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 316a5869057..5dc0b1a3e39 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -20,7 +20,7 @@ from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.config import pyros_config +from pyomo.contrib.pyros.config import pyros_config, resolve_keyword_arguments from pyomo.contrib.pyros.util import ( model_is_valid, recast_to_min_obj, @@ -249,6 +249,48 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): logger.log(msg=f" {key}={val!r}", **log_kwargs) logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + def _resolve_and_validate_pyros_args(self, model, **kwds): + """ + Resolve and validate arguments to ``self.solve()``. + + Parameters + ---------- + model : ConcreteModel + Deterministic model object passed to ``self.solve()``. + **kwds : dict + All other arguments to ``self.solve()``. + + Returns + ------- + config : ConfigDict + Standardized arguments. + + Note + ---- + This method can be broken down into three steps: + + 1. Resolve user arguments based on how they were passed + and order of precedence of the various means by which + they could be passed. + 2. Cast resolved arguments to ConfigDict. Argument-wise + validation is performed automatically. + 3. Inter-argument validation. + """ + options_dict = kwds.pop("options", {}) + dev_options_dict = kwds.pop("dev_options", {}) + resolved_kwds = resolve_keyword_arguments( + prioritized_kwargs_dicts={ + "explicitly": kwds, + "implicitly through argument 'options'": options_dict, + "implicitly through argument 'dev_options'": dev_options_dict, + }, + func=self.solve, + ) + config = self.CONFIG(resolved_kwds) + validate_kwarg_inputs(model, config) + + return config + @document_kwargs_from_configdict( config=CONFIG, section="Keyword Arguments", @@ -299,24 +341,15 @@ def solve( Summary of PyROS termination outcome. """ - - # === Add the explicit arguments to the config - config = self.CONFIG(kwds.pop('options', {})) - config.first_stage_variables = first_stage_variables - config.second_stage_variables = second_stage_variables - config.uncertain_params = uncertain_params - config.uncertainty_set = uncertainty_set - config.local_solver = local_solver - config.global_solver = global_solver - - dev_options = kwds.pop('dev_options', {}) - config.set_value(kwds) - config.set_value(dev_options) - - model = model - - # === Validate kwarg inputs - validate_kwarg_inputs(model, config) + kwds.update(dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + )) + config = self._resolve_and_validate_pyros_args(model, **kwds) # === Validate ability of grcs RO solver to handle this model if not model_is_valid(model): diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 821b1fe7d1e..8308708e080 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -20,6 +20,7 @@ PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, + resolve_keyword_arguments, SolverIterable, SolverResolvable, UncertaintySetDomain, @@ -688,5 +689,70 @@ def test_logger_type(self): standardizer_func(2) +class TestResolveKeywordArguments(unittest.TestCase): + """ + Test keyword argument resolution function works as expected. + """ + + def test_resolve_kwargs_simple_dict(self): + """ + Test resolve kwargs works, simple example + where there is overlap. + """ + explicit_kwargs = dict(arg1=1) + implicit_kwargs_1 = dict(arg1=2, arg2=3) + implicit_kwargs_2 = dict(arg1=4, arg2=4, arg3=5) + + # expected answer + expected_resolved_kwargs = dict(arg1=1, arg2=3, arg3=5) + + # attempt kwargs resolve + with LoggingIntercept(level=logging.WARNING) as LOG: + resolved_kwargs = resolve_keyword_arguments( + prioritized_kwargs_dicts={ + "explicitly": explicit_kwargs, + "implicitly through set 1": implicit_kwargs_1, + "implicitly through set 2": implicit_kwargs_2, + } + ) + + # check kwargs resolved as expected + self.assertEqual( + resolved_kwargs, + expected_resolved_kwargs, + msg="Resolved kwargs do not match expected value.", + ) + + # extract logger warning messages + warning_msgs = LOG.getvalue().split("\n")[:-1] + + self.assertEqual( + len(warning_msgs), 3, msg="Number of warning messages is not as expected." + ) + + # check contents of warning msgs + self.assertRegex( + warning_msgs[0], + expected_regex=( + r"Arguments \['arg1'\] passed implicitly through set 1 " + r"already passed explicitly.*" + ), + ) + self.assertRegex( + warning_msgs[1], + expected_regex=( + r"Arguments \['arg1'\] passed implicitly through set 2 " + r"already passed explicitly.*" + ), + ) + self.assertRegex( + warning_msgs[2], + expected_regex=( + r"Arguments \['arg2'\] passed implicitly through set 2 " + r"already passed implicitly through set 1.*" + ), + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index a75aa4dcf41..071a579018b 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6389,5 +6389,145 @@ def test_pyros_unavailable_backup_subsolver(self): ) +class TestPyROSResolveKwargs(unittest.TestCase): + """ + Test PyROS resolves kwargs as expected. + """ + + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_pyros_kwargs_with_overlap(self): + """ + Test PyROS works as expected when there is overlap between + keyword arguments passed explicitly and implicitly + through `options` or `dev_options`. + """ + # define model + m = ConcreteModel() + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.x3 = Var(initialize=0, bounds=(None, None)) + m.u1 = Param(initialize=1.125, mutable=True) + m.u2 = Param(initialize=1, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u1 ** (0.5) - m.x2 * m.u1 <= 2) + m.con2 = Constraint(expr=m.x1**2 - m.x2**2 * m.u1 == m.x3) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - m.u2) ** 2) + + # Define the uncertainty set + # we take the parameter `u2` to be 'fixed' + ellipsoid = AxisAlignedEllipsoidalSet(center=[1.125, 1], half_lengths=[1, 0]) + + # Instantiate the PyROS solver + pyros_solver = SolverFactory("pyros") + + # Define subsolvers utilized in the algorithm + local_subsolver = SolverFactory('ipopt') + global_subsolver = SolverFactory("baron") + + # Call the PyROS solver + with LoggingIntercept(level=logging.WARNING) as LOG: + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u1, m.u2], + uncertainty_set=ellipsoid, + local_solver=local_subsolver, + global_solver=global_subsolver, + bypass_local_separation=True, + solve_master_globally=True, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": False, + }, + dev_options={ + "objective_focus": ObjectiveType.nominal, + "solve_master_globally": False, + "max_iter": 1, + "time_limit": 1e3, + }, + ) + + # extract warning-level messages. + warning_msgs = LOG.getvalue().split("\n")[:-1] + resolve_kwargs_warning_msgs = [ + msg + for msg in warning_msgs + if msg.startswith("Arguments [") + and "Consider modifying your arguments" in msg + ] + self.assertEqual( + len(resolve_kwargs_warning_msgs), + 3, + msg="Number of warning-level messages not as expected.", + ) + + self.assertRegex( + resolve_kwargs_warning_msgs[0], + expected_regex=( + r"Arguments \['solve_master_globally'\] passed " + r"implicitly through argument 'options' " + r"already passed .*explicitly.*" + ), + ) + self.assertRegex( + resolve_kwargs_warning_msgs[1], + expected_regex=( + r"Arguments \['solve_master_globally'\] passed " + r"implicitly through argument 'dev_options' " + r"already passed .*explicitly.*" + ), + ) + self.assertRegex( + resolve_kwargs_warning_msgs[2], + expected_regex=( + r"Arguments \['objective_focus'\] passed " + r"implicitly through argument 'dev_options' " + r"already passed .*implicitly through argument 'options'.*" + ), + ) + + # check termination status as expected + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.max_iter, + msg="Termination condition not as expected", + ) + self.assertEqual( + results.iterations, 1, msg="Number of iterations not as expected" + ) + + # check config resolved as expected + config = results.config + self.assertEqual( + config.bypass_local_separation, + True, + msg="Resolved value of kwarg `bypass_local_separation` not as expected.", + ) + self.assertEqual( + config.solve_master_globally, + True, + msg="Resolved value of kwarg `solve_master_globally` not as expected.", + ) + self.assertEqual( + config.max_iter, + 1, + msg="Resolved value of kwarg `max_iter` not as expected.", + ) + self.assertEqual( + config.objective_focus, + ObjectiveType.worst_case, + msg="Resolved value of kwarg `objective_focus` not as expected.", + ) + self.assertEqual( + config.time_limit, + 1e3, + msg="Resolved value of kwarg `time_limit` not as expected.", + ) + + if __name__ == "__main__": unittest.main() From adc9d68ee4414ae5adf7568a36ad15d2a2f25aeb Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 15:56:24 -0500 Subject: [PATCH 0287/1178] Make advanced validation more rigorous --- pyomo/contrib/pyros/pyros.py | 26 +- pyomo/contrib/pyros/tests/test_grcs.py | 427 +++++++++++++++++++++++-- pyomo/contrib/pyros/util.py | 418 ++++++++++++++++++------ 3 files changed, 723 insertions(+), 148 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 5dc0b1a3e39..0b61798483c 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -22,16 +22,14 @@ from pyomo.opt import SolverFactory from pyomo.contrib.pyros.config import pyros_config, resolve_keyword_arguments from pyomo.contrib.pyros.util import ( - model_is_valid, recast_to_min_obj, add_decision_rule_constraints, add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, ObjectiveType, - validate_uncertainty_set, identify_objective_functions, - validate_kwarg_inputs, + validate_pyros_inputs, transform_to_standard_form, turn_bounds_to_constraints, replace_uncertain_bounds_with_constraints, @@ -287,7 +285,7 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): func=self.solve, ) config = self.CONFIG(resolved_kwds) - validate_kwarg_inputs(model, config) + validate_pyros_inputs(model, config) return config @@ -351,23 +349,6 @@ def solve( )) config = self._resolve_and_validate_pyros_args(model, **kwds) - # === Validate ability of grcs RO solver to handle this model - if not model_is_valid(model): - raise AttributeError( - "This model structure is not currently handled by the ROSolver." - ) - - # === Define nominal point if not specified - if len(config.nominal_uncertain_param_vals) == 0: - config.nominal_uncertain_param_vals = list( - p.value for p in config.uncertain_params - ) - elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): - raise AttributeError( - "The nominal_uncertain_param_vals list must be the same length" - "as the uncertain_params list" - ) - # === Create data containers model_data = ROSolveResults() model_data.timing = Bunch() @@ -403,9 +384,6 @@ def solve( model.add_component(model_data.util_block, util) # Note: model.component(model_data.util_block) is util - # === Validate uncertainty set happens here, requires util block for Cardinality and FactorModel sets - validate_uncertainty_set(config=config) - # === Leads to a logger warning here for inactive obj when cloning model_data.original_model = model # === For keeping track of variables after cloning diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 071a579018b..b1546fc62e9 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -18,7 +18,6 @@ selective_clone, add_decision_rule_variables, add_decision_rule_constraints, - model_is_valid, turn_bounds_to_constraints, transform_to_standard_form, ObjectiveType, @@ -610,21 +609,6 @@ def test_dr_eqns_form_correct(self): ) -class testModelIsValid(unittest.TestCase): - def test_model_is_valid_via_possible_inputs(self): - m = ConcreteModel() - m.x = Var() - m.obj1 = Objective(expr=m.x**2) - self.assertTrue(model_is_valid(m)) - m.obj2 = Objective(expr=m.x) - self.assertFalse(model_is_valid(m)) - m.obj2.deactivate() - self.assertTrue(model_is_valid(m)) - m.del_component("obj1") - m.del_component("obj2") - self.assertFalse(model_is_valid(m)) - - class testTurnBoundsToConstraints(unittest.TestCase): def test_bounds_to_constraints(self): m = ConcreteModel() @@ -5406,16 +5390,14 @@ def test_multiple_objs(self): # check validation error raised due to multiple objectives with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 3" ): pyros_solver.solve(**solve_kwargs) # check validation error raised due to multiple objectives m.b.obj.deactivate() with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 2" ): pyros_solver.solve(**solve_kwargs) @@ -6529,5 +6511,410 @@ def test_pyros_kwargs_with_overlap(self): ) +class SimpleTestSolver: + """ + Simple test solver class with no actual solve() + functionality. Written to test unrelated aspects + of PyROS functionality. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + res = SolverResults() + res.solver.termination_condition = TerminationCondition.unknown + + return res + + +class TestPyROSSolverAdvancedValidation(unittest.TestCase): + """ + Test PyROS solver returns expected exception messages + when arguments are invalid. + """ + + def build_simple_test_model(self): + """ + Build simple valid test model. + """ + m = ConcreteModel(name="test_model") + + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.u = Param(initialize=1.125, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u ** (0.5) - m.x2 * m.u <= 2) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) + + return m + + def test_pyros_invalid_model_type(self): + """ + Test PyROS fails if model is not of correct class. + """ + mdl = self.build_simple_test_model() + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Model should be of type.*but is of type.*" + with self.assertRaisesRegex(TypeError, exc_str): + pyros.solve( + model=2, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_multiple_objectives(self): + """ + Test PyROS raises exception if input model has multiple + objectives. + """ + mdl = self.build_simple_test_model() + mdl.obj2 = Objective(expr=(mdl.x1 + mdl.x2)) + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Expected model with exactly 1 active.*but.*has 2" + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_empty_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are no + first-stage variables or second-stage variables. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_overlap_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are Vars + passed as both first-stage and second-stage. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." + ) + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x1, mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars were found in both `first_stage_variables`" + "and `second_stage_variables`.*" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x1'") + self.assertRegex( + text=log_msgs[2], + expected_regex="Ensure no Vars are included in both arguments.", + ) + + def test_pyros_vars_not_in_model(self): + """ + Test PyROS appropriately raises exception if there are + variables not included in active model objective + or constraints which are not descended from model. + """ + # set up model + mdl = self.build_simple_test_model() + mdl.name = "model1" + mdl2 = self.build_simple_test_model() + mdl2.name = "model2" + + # set up solvers + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + pyros = SolverFactory("pyros") + + mdl.bad_con = Constraint(expr=mdl2.x1 + mdl2.x2 >= 1) + + desc_dof_map = [ + ("first-stage", [mdl2.x1], [], 2), + ("second-stage", [], [mdl2.x2], 2), + ("state", [mdl.x1], [], 3), + ] + + # now perform checks + for vardesc, first_stage_vars, second_stage_vars, numlines in desc_dof_map: + with LoggingIntercept(level=logging.ERROR) as LOG: + exc_str = ( + "Found entries of " + f"{vardesc} variables not descended from.*model.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=first_stage_vars, + second_stage_variables=second_stage_vars, + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + log_msgs = LOG.getvalue().split("\n")[:-1] + + # check detailed log message is as expected + self.assertEqual( + len(log_msgs), + numlines, + "Error-level log message does not contain expected number of lines.", + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + f"The following {vardesc} variables" + ".*not descended from.*model with name 'model1'" + ), + ) + + def test_pyros_non_continuous_vars(self): + """ + Test PyROS raises exception if model contains + non-continuous variables. + """ + # build model; make one variable discrete + mdl = self.build_simple_test_model() + mdl.x2.domain = NonNegativeIntegers + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = "Model with name 'test_model' contains non-continuous Vars." + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars of model with name 'test_model' " + "are non-continuous:" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x2'") + self.assertRegex( + text=log_msgs[2], + expected_regex=( + "Ensure all model variables passed to " "PyROS solver are continuous." + ), + ) + + def test_pyros_uncertainty_dimension_mismatch(self): + """ + Test PyROS solver raises exception if uncertainty + set dimension does not match the number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + r"Length of argument `uncertain_params` does not match dimension " + r"of argument `uncertainty_set` \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2], [0, 1]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_nominal_point_not_in_set(self): + """ + Test PyROS raises exception if nominal point is not in the + uncertainty set. + + NOTE: need executable solvers to solve set bounding problems + for validity checks. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Nominal uncertain parameter realization \[0\] " + "is not a point in the uncertainty set.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0], + ) + + def test_pyros_nominal_point_len_mismatch(self): + """ + Test PyROS raises exception if there is mismatch between length + of nominal uncertain parameter specification and number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Lengths of arguments `uncertain_params` " + r"and `nominal_uncertain_param_vals` " + r"do not match \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0, 1], + ) + + def test_pyros_invalid_bypass_separation(self): + """ + Test PyROS raises exception if both local and + global separation are set to be bypassed. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Arguments `bypass_local_separation` and `bypass_global_separation` " + r"cannot both be True." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + bypass_local_separation=True, + bypass_global_separation=True, + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 30b5d2df427..685ff9ca898 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -512,14 +512,6 @@ def recast_to_min_obj(model, obj): obj.sense = minimize -def model_is_valid(model): - """ - Assess whether model is valid on basis of the number of active - Objectives. A valid model must contain exactly one active Objective. - """ - return len(list(model.component_data_objects(Objective, active=True))) == 1 - - def turn_bounds_to_constraints(variable, model, config=None): ''' Turn the variable in question's "bounds" into direct inequality constraints on the model. @@ -603,41 +595,6 @@ def get_time_from_solver(results): return float("nan") if solve_time is None else solve_time -def validate_uncertainty_set(config): - ''' - Confirm expression output from uncertainty set function references all q in q. - Typecheck the uncertainty_set.q is Params referenced inside of m. - Give warning that the nominal point (default value in the model) is not in the specified uncertainty set. - :param config: solver config - ''' - # === Check that q in UncertaintySet object constraint expression is referencing q in model.uncertain_params - uncertain_params = config.uncertain_params - - # === Non-zero number of uncertain parameters - if len(uncertain_params) == 0: - raise AttributeError( - "Must provide uncertain params, uncertain_params list length is 0." - ) - # === No duplicate parameters - if len(uncertain_params) != len(ComponentSet(uncertain_params)): - raise AttributeError("No duplicates allowed for uncertain param objects.") - # === Ensure nominal point is in the set - if not config.uncertainty_set.point_in_set( - point=config.nominal_uncertain_param_vals - ): - raise AttributeError( - "Nominal point for uncertain parameters must be in the uncertainty set." - ) - # === Check set validity via boundedness and non-emptiness - if not config.uncertainty_set.is_valid(config=config): - raise AttributeError( - "Invalid uncertainty set detected. Check the uncertainty set object to " - "ensure non-emptiness and boundedness." - ) - - return - - def add_bounds_for_uncertain_parameters(model, config): ''' This function solves a set of optimization problems to determine bounds on the uncertain parameters @@ -817,98 +774,351 @@ def replace_uncertain_bounds_with_constraints(model, uncertain_params): v.setlb(None) -def validate_kwarg_inputs(model, config): - ''' - Confirm kwarg inputs satisfy PyROS requirements. - :param model: the deterministic model - :param config: the config for this PyROS instance - :return: - ''' - - # === Check if model is ConcreteModel object - if not isinstance(model, ConcreteModel): - raise ValueError("Model passed to PyROS solver must be a ConcreteModel object.") +def check_components_descended_from_model(model, components, components_name, config): + """ + Check all members in a provided sequence of Pyomo component + objects are descended from a given ConcreteModel object. - first_stage_variables = config.first_stage_variables - second_stage_variables = config.second_stage_variables - uncertain_params = config.uncertain_params + Parameters + ---------- + model : ConcreteModel + Model from which components should all be descended. + components : Iterable of Component + Components of interest. + components_name : str + Brief description or name for the sequence of components. + Used for constructing error messages. + config : ConfigDict + PyROS solver options. - if not config.first_stage_variables and not config.second_stage_variables: - # Must have non-zero DOF + Raises + ------ + ValueError + If at least one entry of `components` is not descended + from `model`. + """ + components_not_in_model = [comp for comp in components if comp.model() is not model] + if components_not_in_model: + comp_names_str = "\n ".join( + f"{comp.name!r}, from model with name {comp.model().name!r}" + for comp in components_not_in_model + ) + config.progress_logger.error( + f"The following {components_name} " + "are not descended from the " + f"input deterministic model with name {model.name!r}:\n " + f"{comp_names_str}" + ) raise ValueError( - "first_stage_variables and " - "second_stage_variables cannot both be empty lists." + f"Found entries of {components_name} " + "not descended from input model. " + "Check logger output messages." ) - if ComponentSet(first_stage_variables) != ComponentSet( - config.first_stage_variables - ): + +def get_state_vars(blk, first_stage_variables, second_stage_variables): + """ + Get state variables of a modeling block. + + The state variables with respect to `blk` are the unfixed + `_VarData` objects participating in the active objective + or constraints descended from `blk` which are not + first-stage variables or second-stage variables. + + Parameters + ---------- + blk : ScalarBlock + Block of interest. + first_stage_variables : Iterable of VarData + First-stage variables. + second_stage_variables : Iterable of VarData + Second-stage variables. + + Yields + ------ + _VarData + State variable. + """ + dof_var_set = ( + ComponentSet(first_stage_variables) + | ComponentSet(second_stage_variables) + ) + for var in get_vars_from_component(blk, (Objective, Constraint)): + is_state_var = not var.fixed and var not in dof_var_set + if is_state_var: + yield var + + +def check_variables_continuous(model, vars, config): + """ + Check that all DOF and state variables of the model + are continuous. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one variable is found to not be continuous. + + Note + ---- + A variable is considered continuous if the `is_continuous()` + method returns True. + """ + non_continuous_vars = [var for var in vars if not var.is_continuous()] + if non_continuous_vars: + non_continuous_vars_str = "\n ".join( + f"{var.name!r}" for var in non_continuous_vars + ) + config.progress_logger.error( + f"The following Vars of model with name {model.name!r} " + f"are non-continuous:\n {non_continuous_vars_str}\n" + "Ensure all model variables passed to PyROS solver are continuous." + ) raise ValueError( - "All elements in first_stage_variables must be Var members of the model object." + f"Model with name {model.name!r} contains non-continuous Vars." ) - if ComponentSet(second_stage_variables) != ComponentSet( - config.second_stage_variables - ): + +def validate_model(model, config): + """ + Validate deterministic model passed to PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Determinstic model. Should have only one active Objective. + config : ConfigDict + PyROS solver options. + + Returns + ------- + ComponentSet + The variables participating in the active Objective + and Constraint expressions of `model`. + + Raises + ------ + TypeError + If model is not of type ConcreteModel. + ValueError + If model does not have exactly one active Objective + component. + """ + # note: only support ConcreteModel. no support for Blocks + if not isinstance(model, ConcreteModel): + raise TypeError( + f"Model should be of type {ConcreteModel.__name__}, " + f"but is of type {type(model).__name__}." + ) + + # active objectives check + active_objs_list = list( + model.component_data_objects(Objective, active=True, descend_into=True) + ) + if len(active_objs_list) != 1: raise ValueError( - "All elements in second_stage_variables must be Var members of the model object." + "Expected model with exactly 1 active objective, but " + f"model provided has {len(active_objs_list)}." ) - if any( - v in ComponentSet(second_stage_variables) - for v in ComponentSet(first_stage_variables) - ): + +def validate_variable_partitioning(model, config): + """ + Check that partitioning of the first-stage variables, + second-stage variables, and uncertain parameters + is valid. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Returns + ------- + list of _VarData + State variables of the model. + + Raises + ------ + ValueError + If first-stage variables and second-stage variables + overlap, or there are no first-stage variables + and no second-stage variables. + """ + # at least one DOF required + if not config.first_stage_variables and not config.second_stage_variables: raise ValueError( - "No common elements allowed between first_stage_variables and second_stage_variables." + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." ) - if ComponentSet(uncertain_params) != ComponentSet(config.uncertain_params): + # ensure no overlap between DOF var sets + overlapping_vars = ComponentSet(config.first_stage_variables) & ComponentSet( + config.second_stage_variables + ) + if overlapping_vars: + overlapping_var_list = "\n ".join(f"{var.name!r}" for var in overlapping_vars) + config.progress_logger.error( + "The following Vars were found in both `first_stage_variables`" + f"and `second_stage_variables`:\n {overlapping_var_list}" + "\nEnsure no Vars are included in both arguments." + ) raise ValueError( - "uncertain_params must be mutable Param members of the model object." + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." ) - if not config.uncertainty_set: + state_vars = list(get_state_vars( + model, + first_stage_variables=config.first_stage_variables, + second_stage_variables=config.second_stage_variables, + )) + var_type_list_map = { + "first-stage variables": config.first_stage_variables, + "second-stage variables": config.second_stage_variables, + "state variables": state_vars, + } + for desc, vars in var_type_list_map.items(): + check_components_descended_from_model( + model=model, + components=vars, + components_name=desc, + config=config, + ) + + all_vars = ( + config.first_stage_variables + + config.second_stage_variables + + state_vars + ) + check_variables_continuous(model, all_vars, config) + + return state_vars + + +def validate_uncertainty_specification(model, config): + """ + Validate specification of uncertain parameters and uncertainty + set. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one of the following holds: + + - dimension of uncertainty set does not equal number of + uncertain parameters + - uncertainty set `is_valid()` method does not return + true. + - nominal parameter realization is not in the uncertainty set. + """ + check_components_descended_from_model( + model=model, + components=config.uncertain_params, + components_name="uncertain parameters", + config=config, + ) + + if len(config.uncertain_params) != config.uncertainty_set.dim: raise ValueError( - "An UncertaintySet object must be provided to the PyROS solver." + "Length of argument `uncertain_params` does not match dimension " + "of argument `uncertainty_set` " + f"({len(config.uncertain_params)} != {config.uncertainty_set.dim})." ) - non_mutable_params = [] - for p in config.uncertain_params: - if not ( - not p.is_constant() and p.is_fixed() and not p.is_potentially_variable() - ): - non_mutable_params.append(p) - if non_mutable_params: - raise ValueError( - "Param objects which are uncertain must have attribute mutable=True. " - "Offending Params: %s" % [p.name for p in non_mutable_params] - ) + # validate uncertainty set + if not config.uncertainty_set.is_valid(config=config): + raise ValueError( + f"Uncertainty set {config.uncertainty_set} is invalid, " + "as it is either empty or unbounded." + ) - # === Solvers provided check - if not config.local_solver or not config.global_solver: + # fill-in nominal point as necessary, if not provided. + # otherwise, check length matches uncertainty dimension + if not config.nominal_uncertain_param_vals: + config.nominal_uncertain_param_vals = [ + value(param, exception=True) for param in config.uncertain_params + ] + elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): raise ValueError( - "User must designate both a local and global optimization solver via the local_solver" - " and global_solver options." + "Lengths of arguments `uncertain_params` and " + "`nominal_uncertain_param_vals` " + "do not match " + f"({len(config.uncertain_params)} != " + f"{len(config.nominal_uncertain_param_vals)})." ) - if config.bypass_local_separation and config.bypass_global_separation: + # uncertainty set should contain nominal point + nominal_point_in_set = config.uncertainty_set.point_in_set( + point=config.nominal_uncertain_param_vals + ) + if not nominal_point_in_set: raise ValueError( - "User cannot simultaneously enable options " - "'bypass_local_separation' and " - "'bypass_global_separation'." + "Nominal uncertain parameter realization " + f"{config.nominal_uncertain_param_vals} " + "is not a point in the uncertainty set " + f"{config.uncertainty_set!r}." ) - # === Degrees of freedom provided check - if len(config.first_stage_variables) + len(config.second_stage_variables) == 0: + +def validate_separation_problem_options(model, config): + """ + Validate separation problem arguments to the PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If options `bypass_local_separation` and + `bypass_global_separation` are set to False. + """ + if config.bypass_local_separation and config.bypass_global_separation: raise ValueError( - "User must designate at least one first- and/or second-stage variable." + "Arguments `bypass_local_separation` " + "and `bypass_global_separation` " + "cannot both be True." ) - # === Uncertain params provided check - if len(config.uncertain_params) == 0: - raise ValueError("User must designate at least one uncertain parameter.") - return +def validate_pyros_inputs(model, config): + """ + Perform advanced validation of PyROS solver arguments. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + """ + validate_model(model, config) + state_vars = validate_variable_partitioning(model, config) + validate_uncertainty_specification(model, config) + validate_separation_problem_options(model, config) + + return state_vars def substitute_ssv_in_dr_constraints(model, constraint): From 4f64c4fa651ffd48cd6a6e5b7436e1002310b539 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 16:01:31 -0500 Subject: [PATCH 0288/1178] Simplify assembly of state variables --- pyomo/contrib/pyros/pyros.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0b61798483c..05512ec777b 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -12,7 +12,7 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging from pyomo.common.config import document_kwargs_from_configdict -from pyomo.common.collections import Bunch, ComponentSet +from pyomo.common.collections import Bunch from pyomo.core.base.block import Block from pyomo.core.expr import value from pyomo.core.base.var import Var @@ -285,9 +285,9 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): func=self.solve, ) config = self.CONFIG(resolved_kwds) - validate_pyros_inputs(model, config) + state_vars = validate_pyros_inputs(model, config) - return config + return config, state_vars @document_kwargs_from_configdict( config=CONFIG, @@ -347,7 +347,7 @@ def solve( local_solver=local_solver, global_solver=global_solver, )) - config = self._resolve_and_validate_pyros_args(model, **kwds) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) # === Create data containers model_data = ROSolveResults() @@ -378,6 +378,7 @@ def solve( util = Block(concrete=True) util.first_stage_variables = config.first_stage_variables util.second_stage_variables = config.second_stage_variables + util.state_vars = state_vars util.uncertain_params = config.uncertain_params model_data.util_block = unique_component_name(model, 'util') @@ -425,22 +426,10 @@ def solve( # === Move bounds on control variables to explicit ineq constraints wm_util = model_data.working_model - # === Every non-fixed variable that is neither first-stage - # nor second-stage is taken to be a state variable - fsv = ComponentSet(model_data.working_model.util.first_stage_variables) - ssv = ComponentSet(model_data.working_model.util.second_stage_variables) - sv = ComponentSet() - model_data.working_model.util.state_vars = [] - for v in model_data.working_model.component_data_objects(Var): - if not v.fixed and v not in fsv | ssv | sv: - model_data.working_model.util.state_vars.append(v) - sv.add(v) - - # Bounds on second stage variables and state variables are separation objectives, - # they are brought in this was as explicit constraints + # cast bounds on second-stage and state variables to + # explicit constraints for separation objectives for c in model_data.working_model.util.second_stage_variables: turn_bounds_to_constraints(c, wm_util, config) - for c in model_data.working_model.util.state_vars: turn_bounds_to_constraints(c, wm_util, config) From 969aac9d0fbe684af4ed74fdb4371a93416bdd34 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 16:45:33 -0500 Subject: [PATCH 0289/1178] Apply black --- pyomo/contrib/pyros/pyros.py | 18 ++++++++------- pyomo/contrib/pyros/tests/test_config.py | 8 ++----- pyomo/contrib/pyros/util.py | 28 ++++++++++-------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 05512ec777b..0b37b8e9615 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -339,14 +339,16 @@ def solve( Summary of PyROS termination outcome. """ - kwds.update(dict( - first_stage_variables=first_stage_variables, - second_stage_variables=second_stage_variables, - uncertain_params=uncertain_params, - uncertainty_set=uncertainty_set, - local_solver=local_solver, - global_solver=global_solver, - )) + kwds.update( + dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + ) + ) config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) # === Create data containers diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 8308708e080..37587fcce58 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -643,16 +643,12 @@ def test_positive_int_or_minus_one(self): self.assertIs( standardizer_func(1.0), 1, - msg=( - f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." - ), + msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), ) self.assertEqual( standardizer_func(-1.00), -1, - msg=( - f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." - ), + msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), ) exc_str = r"Expected positive int or -1, but received value.*" diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 685ff9ca898..bcd2363bc43 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -839,9 +839,8 @@ def get_state_vars(blk, first_stage_variables, second_stage_variables): _VarData State variable. """ - dof_var_set = ( - ComponentSet(first_stage_variables) - | ComponentSet(second_stage_variables) + dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( + second_stage_variables ) for var in get_vars_from_component(blk, (Objective, Constraint)): is_state_var = not var.fixed and var not in dof_var_set @@ -977,11 +976,13 @@ def validate_variable_partitioning(model, config): "contain at least one common Var object." ) - state_vars = list(get_state_vars( - model, - first_stage_variables=config.first_stage_variables, - second_stage_variables=config.second_stage_variables, - )) + state_vars = list( + get_state_vars( + model, + first_stage_variables=config.first_stage_variables, + second_stage_variables=config.second_stage_variables, + ) + ) var_type_list_map = { "first-stage variables": config.first_stage_variables, "second-stage variables": config.second_stage_variables, @@ -989,17 +990,10 @@ def validate_variable_partitioning(model, config): } for desc, vars in var_type_list_map.items(): check_components_descended_from_model( - model=model, - components=vars, - components_name=desc, - config=config, + model=model, components=vars, components_name=desc, config=config ) - all_vars = ( - config.first_stage_variables - + config.second_stage_variables - + state_vars - ) + all_vars = config.first_stage_variables + config.second_stage_variables + state_vars check_variables_continuous(model, all_vars, config) return state_vars From 8201978d5536c84e465e1470cf72ea4c02868cfc Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 17:40:34 -0500 Subject: [PATCH 0290/1178] Make first char of test class names uppercase --- pyomo/contrib/pyros/tests/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 37587fcce58..50152abbacd 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -30,7 +30,7 @@ from pyomo.contrib.pyros.uncertainty_sets import BoxSet -class testInputDataStandardizer(unittest.TestCase): +class TestInputDataStandardizer(unittest.TestCase): """ Test standardizer method for Pyomo component-type inputs. """ @@ -543,7 +543,7 @@ def test_config_objective_focus(self): config.objective_focus = invalid_focus -class testPathLikeOrNone(unittest.TestCase): +class TestPathLikeOrNone(unittest.TestCase): """ Test interface for validating path-like arguments. """ From 012e319dbbea4554cf44c4fc5cb255ae8a014eee Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 17:47:10 -0500 Subject: [PATCH 0291/1178] Remove support for solver argument `dev_options` --- pyomo/contrib/pyros/pyros.py | 2 -- pyomo/contrib/pyros/tests/test_grcs.py | 24 ++---------------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0b37b8e9615..69a6ce315da 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -275,12 +275,10 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): 3. Inter-argument validation. """ options_dict = kwds.pop("options", {}) - dev_options_dict = kwds.pop("dev_options", {}) resolved_kwds = resolve_keyword_arguments( prioritized_kwargs_dicts={ "explicitly": kwds, "implicitly through argument 'options'": options_dict, - "implicitly through argument 'dev_options'": dev_options_dict, }, func=self.solve, ) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index b1546fc62e9..5727df70a52 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6424,12 +6424,8 @@ def test_pyros_kwargs_with_overlap(self): options={ "objective_focus": ObjectiveType.worst_case, "solve_master_globally": False, - }, - dev_options={ - "objective_focus": ObjectiveType.nominal, - "solve_master_globally": False, "max_iter": 1, - "time_limit": 1e3, + "time_limit": 1000, }, ) @@ -6443,7 +6439,7 @@ def test_pyros_kwargs_with_overlap(self): ] self.assertEqual( len(resolve_kwargs_warning_msgs), - 3, + 1, msg="Number of warning-level messages not as expected.", ) @@ -6455,22 +6451,6 @@ def test_pyros_kwargs_with_overlap(self): r"already passed .*explicitly.*" ), ) - self.assertRegex( - resolve_kwargs_warning_msgs[1], - expected_regex=( - r"Arguments \['solve_master_globally'\] passed " - r"implicitly through argument 'dev_options' " - r"already passed .*explicitly.*" - ), - ) - self.assertRegex( - resolve_kwargs_warning_msgs[2], - expected_regex=( - r"Arguments \['objective_focus'\] passed " - r"implicitly through argument 'dev_options' " - r"already passed .*implicitly through argument 'options'.*" - ), - ) # check termination status as expected self.assertEqual( From 2c4b89e1116a79718c2b8d72650b88ca8a09e6bb Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 17:48:04 -0500 Subject: [PATCH 0292/1178] Remove `dev_options` from test docstring --- pyomo/contrib/pyros/tests/test_grcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 5727df70a52..f8c4078f4ee 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6383,7 +6383,7 @@ def test_pyros_kwargs_with_overlap(self): """ Test PyROS works as expected when there is overlap between keyword arguments passed explicitly and implicitly - through `options` or `dev_options`. + through `options`. """ # define model m = ConcreteModel() From 55884a4ab15b8cc895a5eb3ad0af360f4029bcc0 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 18:48:55 -0500 Subject: [PATCH 0293/1178] Apply black 24.1.1 --- pyomo/contrib/pyros/config.py | 1 - pyomo/contrib/pyros/tests/test_config.py | 1 - pyomo/contrib/pyros/tests/test_grcs.py | 6 +++--- pyomo/contrib/pyros/uncertainty_sets.py | 1 + 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c003d699255..42b4ddc29a0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -2,7 +2,6 @@ Interfaces for managing PyROS solver options. """ - from collections.abc import Iterable import logging import os diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 50152abbacd..a7f40ca37e8 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -2,7 +2,6 @@ Test objects for construction of PyROS ConfigDict. """ - import logging import os import unittest diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index f8c4078f4ee..70f8a9dfb60 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3750,9 +3750,9 @@ def test_solve_master(self): master_data.master_model.scenarios[0, 0].second_stage_objective = Expression( expr=master_data.master_model.scenarios[0, 0].x ) - master_data.master_model.scenarios[ - 0, 0 - ].util.dr_var_to_exponent_map = ComponentMap() + master_data.master_model.scenarios[0, 0].util.dr_var_to_exponent_map = ( + ComponentMap() + ) master_data.iteration = 0 master_data.timing = TimingData() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 4a2f198bc17..963abebb60c 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -276,6 +276,7 @@ class UncertaintySetDomain: """ Domain validator for uncertainty set argument. """ + def __call__(self, obj): """ Type validate uncertainty set object. From cabe4250813473132dd7dc27aab624be584a6194 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 19:03:16 -0500 Subject: [PATCH 0294/1178] Fix typos --- pyomo/contrib/pyros/config.py | 2 +- pyomo/contrib/pyros/tests/test_config.py | 4 ++-- pyomo/contrib/pyros/util.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 42b4ddc29a0..261e4069c03 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -1001,7 +1001,7 @@ def resolve_keyword_arguments(prioritized_kwargs_dicts, func=None): overlapping_args_set = set() for prev_desc, prev_kwargs in prev_prioritized_kwargs_dicts.items(): - # determine overlap between currrent and previous + # determine overlap between current and previous # set of kwargs, and remove overlap of current # and higher priority sets from the result curr_prev_overlapping_args = ( diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index a7f40ca37e8..ec377f96ca6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -199,7 +199,7 @@ def test_standardizer_invalid_str_passed(self): with self.assertRaisesRegex(TypeError, exc_str): standardizer_func("abcd") - def test_standardizer_invalid_unintialized_params(self): + def test_standardizer_invalid_uninitialized_params(self): """ Test standardizer raises exception when Param with uninitialized entries passed. @@ -373,7 +373,7 @@ def test_solver_resolvable_invalid_type(self): def test_solver_resolvable_unavailable_solver(self): """ Test solver standardizer fails in event solver is - unavaiable. + unavailable. """ unavailable_solver = UnavailableSolver() standardizer_func = SolverResolvable( diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index bcd2363bc43..e0ed552aab4 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -892,7 +892,7 @@ def validate_model(model, config): Parameters ---------- model : ConcreteModel - Determinstic model. Should have only one active Objective. + Deterministic model. Should have only one active Objective. config : ConfigDict PyROS solver options. From f7c8e4af1724f0dd8362467e12444f71110c684c Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 19:05:14 -0500 Subject: [PATCH 0295/1178] Fix another typo --- pyomo/contrib/pyros/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 261e4069c03..f12fb3d0be0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -230,7 +230,7 @@ def standardize_ctype_obj(self, obj): def standardize_cdatatype_obj(self, obj): """ - Standarize object of type ``self.cdatatype`` to + Standardize object of type ``self.cdatatype`` to ``[obj]``. """ if self.cdatatype_validator is not None: From 7fdcf248c6961aa97de924fc885b437a32270597 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 19:45:54 -0500 Subject: [PATCH 0296/1178] Check IPOPT available before advanced validation tests --- pyomo/contrib/pyros/tests/test_grcs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 70f8a9dfb60..904c981ed93 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -119,6 +119,9 @@ scip_license_is_valid = False scip_version = (0, 0, 0) +_ipopt = SolverFactory("ipopt") +ipopt_available = _ipopt.available(exception_flag=False) + # @SolverFactory.register("time_delay_solver") class TimeDelaySolver(object): @@ -3533,10 +3536,7 @@ class behaves like a regular Python list. # assigning to slices should work fine all_sets[3:] = [BoxSet([[1, 1.5]]), BoxSet([[1, 3]])] - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_correct_params(self): ''' Case in which the UncertaintySet is constructed using the uncertain_param objects from the model to @@ -3575,10 +3575,7 @@ def test_uncertainty_set_with_correct_params(self): " be the same uncertain param Var objects in the original model.", ) - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_incorrect_params(self): ''' Case in which the set is constructed using uncertain_param objects which are Params instead of @@ -6799,6 +6796,7 @@ def test_pyros_uncertainty_dimension_mismatch(self): global_solver=global_solver, ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_nominal_point_not_in_set(self): """ Test PyROS raises exception if nominal point is not in the @@ -6832,6 +6830,7 @@ def test_pyros_nominal_point_not_in_set(self): nominal_uncertain_param_vals=[0], ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_nominal_point_len_mismatch(self): """ Test PyROS raises exception if there is mismatch between length @@ -6864,6 +6863,7 @@ def test_pyros_nominal_point_len_mismatch(self): nominal_uncertain_param_vals=[0, 1], ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_invalid_bypass_separation(self): """ Test PyROS raises exception if both local and From ec0ad71db2944ec65bfa37f3f64dea94fc942cea Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 20:24:47 -0500 Subject: [PATCH 0297/1178] Remove IPOPT from solver validation tests --- pyomo/contrib/pyros/tests/test_config.py | 40 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index ec377f96ca6..142a14c2122 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -302,6 +302,29 @@ def test_uncertainty_set_domain_invalid_type(self): standardizer_func(2) +AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" + + +@SolverFactory.register(name=AVAILABLE_SOLVER_TYPE_NAME) +class AvailableSolver: + """ + Perenially avaiable placeholder solver. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + return SolverResults() + + class UnavailableSolver: def available(self, exception_flag=True): if exception_flag: @@ -322,7 +345,7 @@ def test_solver_resolvable_valid_str(self): Test solver resolvable class is valid for string type. """ - solver_str = "ipopt" + solver_str = AVAILABLE_SOLVER_TYPE_NAME standardizer_func = SolverResolvable() solver = standardizer_func(solver_str) expected_solver_type = type(SolverFactory(solver_str)) @@ -342,7 +365,7 @@ def test_solver_resolvable_valid_solver_type(self): Test solver resolvable class is valid for string type. """ - solver = SolverFactory("ipopt") + solver = SolverFactory(AVAILABLE_SOLVER_TYPE_NAME) standardizer_func = SolverResolvable() standardized_solver = standardizer_func(solver) @@ -403,8 +426,11 @@ def test_solver_iterable_valid_list(self): Test solver type standardizer works for list of valid objects castable to solver. """ - solver_list = ["ipopt", SolverFactory("ipopt")] - expected_solver_types = [type(SolverFactory("ipopt"))] * 2 + solver_list = [ + AVAILABLE_SOLVER_TYPE_NAME, + SolverFactory(AVAILABLE_SOLVER_TYPE_NAME), + ] + expected_solver_types = [AvailableSolver] * 2 standardizer_func = SolverIterable() standardized_solver_list = standardizer_func(solver_list) @@ -438,7 +464,7 @@ def test_solver_iterable_valid_str(self): """ Test SolverIterable raises exception when str passed. """ - solver_str = "ipopt" + solver_str = AVAILABLE_SOLVER_TYPE_NAME standardizer_func = SolverIterable() solver_list = standardizer_func(solver_str) @@ -450,7 +476,7 @@ def test_solver_iterable_unavailable_solver(self): """ Test SolverIterable addresses unavailable solvers appropriately. """ - solvers = (SolverFactory("ipopt"), UnavailableSolver()) + solvers = (AvailableSolver(), UnavailableSolver()) standardizer_func = SolverIterable( require_available=True, @@ -496,7 +522,7 @@ def test_solver_iterable_invalid_list(self): Test SolverIterable raises exception if iterable contains at least one invalid object. """ - invalid_object = ["ipopt", 2] + invalid_object = [AVAILABLE_SOLVER_TYPE_NAME, 2] standardizer_func = SolverIterable(solver_desc="backup solver") exc_str = ( From 445d4cf7067d08b67d6d73f60a3b8c6a6880b8c7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 20:49:32 -0500 Subject: [PATCH 0298/1178] Fix typos --- pyomo/contrib/pyros/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 142a14c2122..bff098742b6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -308,7 +308,7 @@ def test_uncertainty_set_domain_invalid_type(self): @SolverFactory.register(name=AVAILABLE_SOLVER_TYPE_NAME) class AvailableSolver: """ - Perenially avaiable placeholder solver. + Perennially available placeholder solver. """ def available(self, exception_flag=False): From 7b26268cb626b6e881849b0f429382e0362f6cce Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 22:02:09 -0500 Subject: [PATCH 0299/1178] Fix test solver registration --- pyomo/contrib/pyros/tests/test_config.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index bff098742b6..adae2dbb1e5 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -305,7 +305,6 @@ def test_uncertainty_set_domain_invalid_type(self): AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" -@SolverFactory.register(name=AVAILABLE_SOLVER_TYPE_NAME) class AvailableSolver: """ Perennially available placeholder solver. @@ -340,6 +339,12 @@ class TestSolverResolvable(unittest.TestCase): Test PyROS standardizer for solver-type objects. """ + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + def test_solver_resolvable_valid_str(self): """ Test solver resolvable class is valid for string @@ -421,6 +426,12 @@ class TestSolverIterable(unittest.TestCase): arguments. """ + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + def test_solver_iterable_valid_list(self): """ Test solver type standardizer works for list of valid From b899744ddb4812ee5a76417b4fccf9acc84b3b12 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 22:15:48 -0500 Subject: [PATCH 0300/1178] Check numpy available for uncertainty set test --- pyomo/contrib/pyros/tests/test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index adae2dbb1e5..3113afaac89 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -27,6 +27,7 @@ from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults from pyomo.contrib.pyros.uncertainty_sets import BoxSet +from pyomo.common.dependencies import numpy_available class TestInputDataStandardizer(unittest.TestCase): @@ -280,6 +281,7 @@ class TestUncertaintySetDomain(unittest.TestCase): Test domain validator for uncertainty set arguments. """ + @unittest.skipUnless(numpy_available, "Numpy is not available.") def test_uncertainty_set_domain_valid_set(self): """ Test validator works for valid argument. From 655c06dc713c3fa7155c501663b74d3f1116ae89 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 22:51:55 -0500 Subject: [PATCH 0301/1178] Add IPOPT availability checks to solve tests --- pyomo/contrib/pyros/tests/test_grcs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 904c981ed93..ef029d0f352 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6322,7 +6322,7 @@ def test_pyros_unavailable_subsolver(self): second_stage_variables=[m.z[1]], uncertain_params=[m.p[0]], uncertainty_set=BoxSet([[0, 1]]), - local_solver=SolverFactory("ipopt"), + local_solver=SimpleTestSolver(), global_solver=UnavailableSolver(), ) @@ -6331,6 +6331,7 @@ def test_pyros_unavailable_subsolver(self): error_msgs, r"Output of `available\(\)` method.*global solver.*" ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_unavailable_backup_subsolver(self): """ Test PyROS raises expected error message when @@ -6373,8 +6374,10 @@ class TestPyROSResolveKwargs(unittest.TestCase): Test PyROS resolves kwargs as expected. """ + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." + baron_license_is_valid, + "Global NLP solver is not available and licensed." ) def test_pyros_kwargs_with_overlap(self): """ From 0f727ab36a8135472092d1e0161161936a16e618 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 23:06:31 -0500 Subject: [PATCH 0302/1178] Apply black to tests --- pyomo/contrib/pyros/tests/test_grcs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index ef029d0f352..a94b4d9d408 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6376,8 +6376,7 @@ class TestPyROSResolveKwargs(unittest.TestCase): @unittest.skipUnless(ipopt_available, "IPOPT is not available.") @unittest.skipUnless( - baron_license_is_valid, - "Global NLP solver is not available and licensed." + baron_license_is_valid, "Global NLP solver is not available and licensed." ) def test_pyros_kwargs_with_overlap(self): """ From 08596e575854b5d7414612c804e8babfbb7e6936 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 8 Feb 2024 13:55:05 -0500 Subject: [PATCH 0303/1178] Update version number, changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 8 ++++++++ pyomo/contrib/pyros/pyros.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 7d4678f0ba3..94f4848edb2 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,13 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.10 07 Feb 2024 +------------------------------------------------------------------------------- +- Update argument resolution and validation routines of `PyROS.solve()` +- Use methods of `common.config` for docstring of `PyROS.solve()` + + ------------------------------------------------------------------------------- PyROS 1.2.9 15 Dec 2023 ------------------------------------------------------------------------------- @@ -14,6 +21,7 @@ PyROS 1.2.9 15 Dec 2023 - Refactor DR polishing routine; initialize auxiliary variables to values they are meant to represent + ------------------------------------------------------------------------------- PyROS 1.2.8 12 Oct 2023 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 69a6ce315da..0659ab43a64 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -44,7 +44,7 @@ from datetime import datetime -__version__ = "1.2.9" +__version__ = "1.2.10" default_pyros_solver_logger = setup_pyros_logger() From 73e1e2865c82a57d04bd86c662694dc79513b419 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Feb 2024 14:19:46 -0500 Subject: [PATCH 0304/1178] Limit visibility of option `p_robustness` --- pyomo/contrib/pyros/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index f12fb3d0be0..3256a333fdc 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -945,6 +945,7 @@ def pyros_config(): the nominal parameter realization. """ ), + visibility=1, ), ) From cbbcceb7a1eae4d215053b77d38cb47bc6bc95b5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Feb 2024 15:11:49 -0500 Subject: [PATCH 0305/1178] Add note on `options` arg to docs --- doc/OnlineDocs/contributed_packages/pyros.rst | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 3ff1bfccf0e..d741bb26b5c 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -142,6 +142,7 @@ PyROS Solver Interface Otherwise, the solution returned is certified to only be robust feasible. + PyROS Uncertainty Sets ----------------------------- Uncertainty sets are represented by subclasses of @@ -518,7 +519,7 @@ correspond to first-stage degrees of freedom. >>> # === Designate which variables correspond to first-stage >>> # and second-stage degrees of freedom === - >>> first_stage_variables =[ + >>> first_stage_variables = [ ... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6, ... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31, ... ] @@ -657,6 +658,54 @@ For this example, we notice a ~25% decrease in the final objective value when switching from a static decision rule (no second-stage recourse) to an affine decision rule. + +Specifying Arguments Indirectly Through ``options`` +""""""""""""""""""""""""""""""""""""""""""""""""""" +Like other Pyomo solver interface methods, +:meth:`~pyomo.contrib.pyros.PyROS.solve` +provides support for specifying options indirectly by passing +a keyword argument ``options``, whose value must be a :class:`dict` +mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` +to their desired values. +For example, the ``solve()`` statement in the +:ref:`two-stage problem example ` +could have been equivalently written as: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... options={ + ... "objective_focus": pyros.ObjectiveType.worst_case, + ... "solve_master_globally": True, + ... "decision_rule_order": 1, + ... }, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver. + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + +In the event an argument is passed directly +by position or keyword, *and* indirectly through ``options``, +an appropriate warning is issued, +and the value passed directly takes precedence over the value +passed through ``options``. + + The Price of Robustness """""""""""""""""""""""" In conjunction with standard Python control flow tools, From d4d8d32c31fbb34c77f7b87946c2e26f1b03b715 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Feb 2024 15:12:59 -0500 Subject: [PATCH 0306/1178] Tweak new note wording --- doc/OnlineDocs/contributed_packages/pyros.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index d741bb26b5c..b5b71020a9c 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -668,7 +668,7 @@ a keyword argument ``options``, whose value must be a :class:`dict` mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` to their desired values. For example, the ``solve()`` statement in the -:ref:`two-stage problem example ` +:ref:`two-stage problem snippet ` could have been equivalently written as: .. doctest:: From 3ca70d16458afcfe85e9d9cb9dc9936489cdbd22 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 10 Feb 2024 21:31:15 -0700 Subject: [PATCH 0307/1178] porting appsi_gurobi to contrib/solver --- pyomo/contrib/solver/gurobi.py | 1495 +++++++++++++++++ pyomo/contrib/solver/plugins.py | 2 + pyomo/contrib/solver/sol_reader.py | 4 +- .../tests/solvers/test_gurobi_persistent.py | 691 ++++++++ .../solver/tests/solvers/test_solvers.py | 1350 +++++++++++++++ pyomo/contrib/solver/util.py | 62 +- 6 files changed, 3549 insertions(+), 55 deletions(-) create mode 100644 pyomo/contrib/solver/gurobi.py create mode 100644 pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py create mode 100644 pyomo/contrib/solver/tests/solvers/test_solvers.py diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py new file mode 100644 index 00000000000..2dcdacd320d --- /dev/null +++ b/pyomo/contrib/solver/gurobi.py @@ -0,0 +1,1495 @@ +from collections.abc import Iterable +import logging +import math +from typing import List, Dict, Optional +from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet +from pyomo.common.log import LogStream +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.shutdown import python_is_shutting_down +from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.param import _ParamData +from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types +from pyomo.repn import generate_standard_repn +from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.contrib.solver.config import PersistentBranchAndBoundConfig +from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.core.staleflag import StaleFlagManager +import sys +import datetime +import io +from pyomo.contrib.solver.factory import SolverFactory + +logger = logging.getLogger(__name__) + + +def _import_gurobipy(): + try: + import gurobipy + except ImportError: + Gurobi._available = Gurobi.Availability.NotFound + raise + if gurobipy.GRB.VERSION_MAJOR < 7: + Gurobi._available = Gurobi.Availability.BadVersion + raise ImportError('The APPSI Gurobi interface requires gurobipy>=7.0.0') + return gurobipy + + +gurobipy, gurobipy_available = attempt_import('gurobipy', importer=_import_gurobipy) + + +class DegreeError(PyomoException): + pass + + +class GurobiConfig(PersistentBranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(GurobiConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_mipstart: bool = self.declare( + 'use_mipstart', + ConfigValue( + default=False, + domain=bool, + description="If True, the values of the integer variables will be passed to Gurobi.", + ) + ) + + +class GurobiSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + self._solver._load_vars( + vars_to_load=vars_to_load, solution_number=solution_number + ) + + def get_primals(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + return self._solver._get_primals( + vars_to_load=vars_to_load, solution_number=solution_number + ) + + +class _MutableLowerBound(object): + def __init__(self, expr): + self.var = None + self.expr = expr + + def update(self): + self.var.setAttr('lb', value(self.expr)) + + +class _MutableUpperBound(object): + def __init__(self, expr): + self.var = None + self.expr = expr + + def update(self): + self.var.setAttr('ub', value(self.expr)) + + +class _MutableLinearCoefficient(object): + def __init__(self): + self.expr = None + self.var = None + self.con = None + self.gurobi_model = None + + def update(self): + self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) + + +class _MutableRangeConstant(object): + def __init__(self): + self.lhs_expr = None + self.rhs_expr = None + self.con = None + self.slack_name = None + self.gurobi_model = None + + def update(self): + rhs_val = value(self.rhs_expr) + lhs_val = value(self.lhs_expr) + self.con.rhs = rhs_val + slack = self.gurobi_model.getVarByName(self.slack_name) + slack.ub = rhs_val - lhs_val + + +class _MutableConstant(object): + def __init__(self): + self.expr = None + self.con = None + + def update(self): + self.con.rhs = value(self.expr) + + +class _MutableQuadraticConstraint(object): + def __init__( + self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs + ): + self.con = gurobi_con + self.gurobi_model = gurobi_model + self.constant = constant + self.last_constant_value = value(self.constant.expr) + self.linear_coefs = linear_coefs + self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs] + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + gurobi_expr = self.gurobi_model.getQCRow(self.con) + for ndx, coef in enumerate(self.linear_coefs): + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_linear_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var + self.last_linear_coef_values[ndx] = current_coef_value + for ndx, coef in enumerate(self.quadratic_coefs): + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_quadratic_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = current_coef_value + return gurobi_expr + + def get_updated_rhs(self): + return value(self.constant.expr) + + +class _MutableObjective(object): + def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): + self.gurobi_model = gurobi_model + self.constant = constant + self.linear_coefs = linear_coefs + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + for ndx, coef in enumerate(self.linear_coefs): + coef.var.obj = value(coef.expr) + self.gurobi_model.ObjCon = value(self.constant.expr) + + gurobi_expr = None + for ndx, coef in enumerate(self.quadratic_coefs): + if value(coef.expr) != self.last_quadratic_coef_values[ndx]: + if gurobi_expr is None: + self.gurobi_model.update() + gurobi_expr = self.gurobi_model.getObjective() + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_quadratic_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = current_coef_value + return gurobi_expr + + +class _MutableQuadraticCoefficient(object): + def __init__(self): + self.expr = None + self.var1 = None + self.var2 = None + + +class Gurobi(PersistentSolverUtils, PersistentSolverBase): + """ + Interface to Gurobi + """ + + CONFIG = GurobiConfig() + + _available = None + _num_instances = 0 + + def __init__(self, **kwds): + PersistentSolverUtils.__init__(self) + PersistentSolverBase.__init__(self, **kwds) + self._num_instances += 1 + self._solver_model = None + self._symbol_map = SymbolMap() + self._labeler = None + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() + self._range_constraints = OrderedSet() + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._mutable_quadratic_helpers = dict() + self._mutable_objective = None + self._needs_updated = True + self._callback = None + self._callback_func = None + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._last_results_object: Optional[Results] = None + self._config: Optional[GurobiConfig] = None + + def available(self): + if not gurobipy_available: # this triggers the deferred import + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + return self._check_license() + + def _check_license(self): + avail = False + try: + # Gurobipy writes out license file information when creating + # the environment + with capture_output(capture_fd=True): + m = gurobipy.Model() + if self._solver_model is None: + self._solver_model = m + avail = True + except gurobipy.GurobiError: + avail = False + + if avail: + if self._available is None: + res = Gurobi._check_full_license() + self._available = res + return res + else: + return self._available + else: + return self.Availability.BadLicense + + @classmethod + def _check_full_license(cls): + m = gurobipy.Model() + m.setParam('OutputFlag', 0) + try: + m.addVars(range(2001)) + m.optimize() + return cls.Availability.FullLicense + except gurobipy.GurobiError: + return cls.Availability.LimitedLicense + + def release_license(self): + self._reinit() + if gurobipy_available: + with capture_output(capture_fd=True): + gurobipy.disposeDefaultEnv() + + def __del__(self): + if not python_is_shutting_down(): + self._num_instances -= 1 + if self._num_instances == 0: + self.release_license() + + def version(self): + version = ( + gurobipy.GRB.VERSION_MAJOR, + gurobipy.GRB.VERSION_MINOR, + gurobipy.GRB.VERSION_TECHNICAL, + ) + return version + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self): + config = self._config + timer = config.timer + ostreams = [io.StringIO()] + if config.tee: + ostreams.append(sys.stdout) + if config.log_solver_output: + ostreams.append(LogStream(level=logging.INFO, logger=logger)) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + options = config.solver_options + + self._solver_model.setParam('LogToConsole', 1) + + if config.threads is not None: + self._solver_model.setParam('Threads', config.threads) + if config.time_limit is not None: + self._solver_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + self._solver_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + for pyomo_var_id, gurobi_var in self._pyomo_var_to_solver_var_map.items(): + pyomo_var = self._vars[pyomo_var_id][0] + if pyomo_var.is_integer() and pyomo_var.value is not None: + self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) + + for key, option in options.items(): + self._solver_model.setParam(key, option) + + timer.start('optimize') + self._solver_model.optimize(self._callback) + timer.stop('optimize') + + self._needs_updated = False + res = self._postsolve(timer) + res.solver_configuration = config + res.solver_name = 'Gurobi' + res.solver_version = self.version() + res.solver_log = ostreams[0].getvalue() + return res + + def solve(self, model, **kwds) -> Results: + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + self._config = config = self.config(value=kwds, preserve_implicit=True) + StaleFlagManager.mark_all_as_stale() + # Note: solver availability check happens in set_instance(), + # which will be called (either by the user before this call, or + # below) before this method calls self._solve. + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if config.timer is None: + config.timer = HierarchicalTimer() + timer = config.timer + if model is not self._model: + timer.start('set_instance') + self.set_instance(model) + timer.stop('set_instance') + else: + timer.start('update') + self.update(timer=timer) + timer.stop('update') + res = self._solve() + self._last_results_object = res + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + res.timing_info.start_timestamp = start_timestamp + res.timing_info.wall_time = (end_timestamp - start_timestamp).total_seconds() + res.timing_info.timer = timer + return res + + def _process_domain_and_bounds( + self, var, var_id, mutable_lbs, mutable_ubs, ndx, gurobipy_var + ): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if lb is None: + lb = -gurobipy.GRB.INFINITY + if ub is None: + ub = gurobipy.GRB.INFINITY + if step == 0: + vtype = gurobipy.GRB.CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = gurobipy.GRB.BINARY + else: + vtype = gurobipy.GRB.INTEGER + else: + raise ValueError( + f'Unrecognized domain step: {step} (should be either 0 or 1)' + ) + if _fixed: + lb = _value + ub = _value + else: + if _lb is not None: + if not is_constant(_lb): + mutable_bound = _MutableLowerBound(NPV_MaxExpression((_lb, lb))) + if gurobipy_var is None: + mutable_lbs[ndx] = mutable_bound + else: + mutable_bound.var = gurobipy_var + self._mutable_bounds[var_id, 'lb'] = (var, mutable_bound) + lb = max(value(_lb), lb) + if _ub is not None: + if not is_constant(_ub): + mutable_bound = _MutableUpperBound(NPV_MinExpression((_ub, ub))) + if gurobipy_var is None: + mutable_ubs[ndx] = mutable_bound + else: + mutable_bound.var = gurobipy_var + self._mutable_bounds[var_id, 'ub'] = (var, mutable_bound) + ub = min(value(_ub), ub) + + return lb, ub, vtype + + def _add_variables(self, variables: List[_GeneralVarData]): + var_names = list() + vtypes = list() + lbs = list() + ubs = list() + mutable_lbs = dict() + mutable_ubs = dict() + for ndx, var in enumerate(variables): + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds( + var, id(var), mutable_lbs, mutable_ubs, ndx, None + ) + var_names.append(varname) + vtypes.append(vtype) + lbs.append(lb) + ubs.append(ub) + + gurobi_vars = self._solver_model.addVars( + len(variables), lb=lbs, ub=ubs, vtype=vtypes, name=var_names + ) + + for ndx, pyomo_var in enumerate(variables): + gurobi_var = gurobi_vars[ndx] + self._pyomo_var_to_solver_var_map[id(pyomo_var)] = gurobi_var + for ndx, mutable_bound in mutable_lbs.items(): + mutable_bound.var = gurobi_vars[ndx] + for ndx, mutable_bound in mutable_ubs.items(): + mutable_bound.var = gurobi_vars[ndx] + self._vars_added_since_update.update(variables) + self._needs_updated = True + + def _add_params(self, params: List[_ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_tmp_config = self._config + self.__init__() + self.config = saved_config + self._config = saved_tmp_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f'Solver {c.__module__}.{c.__qualname__} is not available ' + f'({self.available()}).' + ) + self._reinit() + self._model = model + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler('x') + + if model.name is not None: + self._solver_model = gurobipy.Model(model.name) + else: + self._solver_model = gurobipy.Model() + + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + def _get_expr_from_pyomo_expr(self, expr): + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + repn = generate_standard_repn(expr, quadratic=True, compute_values=False) + + degree = repn.polynomial_degree() + if (degree is None) or (degree > 2): + raise DegreeError( + 'GurobiAuto does not support expressions of degree {0}.'.format(degree) + ) + + if len(repn.linear_vars) > 0: + linear_coef_vals = list() + for ndx, coef in enumerate(repn.linear_coefs): + if not is_constant(coef): + mutable_linear_coefficient = _MutableLinearCoefficient() + mutable_linear_coefficient.expr = coef + mutable_linear_coefficient.var = self._pyomo_var_to_solver_var_map[ + id(repn.linear_vars[ndx]) + ] + mutable_linear_coefficients.append(mutable_linear_coefficient) + linear_coef_vals.append(value(coef)) + new_expr = gurobipy.LinExpr( + linear_coef_vals, + [self._pyomo_var_to_solver_var_map[id(i)] for i in repn.linear_vars], + ) + else: + new_expr = 0.0 + + for ndx, v in enumerate(repn.quadratic_vars): + x, y = v + gurobi_x = self._pyomo_var_to_solver_var_map[id(x)] + gurobi_y = self._pyomo_var_to_solver_var_map[id(y)] + coef = repn.quadratic_coefs[ndx] + if not is_constant(coef): + mutable_quadratic_coefficient = _MutableQuadraticCoefficient() + mutable_quadratic_coefficient.expr = coef + mutable_quadratic_coefficient.var1 = gurobi_x + mutable_quadratic_coefficient.var2 = gurobi_y + mutable_quadratic_coefficients.append(mutable_quadratic_coefficient) + coef_val = value(coef) + new_expr += coef_val * gurobi_x * gurobi_y + + return ( + new_expr, + repn.constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + + def _add_constraints(self, cons: List[_GeneralConstraintData]): + for con in cons: + conname = self._symbol_map.getSymbol(con, self._labeler) + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if ( + gurobi_expr.__class__ in {gurobipy.LinExpr, gurobipy.Var} + or gurobi_expr.__class__ in native_numeric_types + ): + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + elif con.has_lb() and con.has_ub(): + lhs_expr = con.lower - repn_constant + rhs_expr = con.upper - repn_constant + lhs_val = value(lhs_expr) + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addRange( + gurobi_expr, lhs_val, rhs_val, name=conname + ) + self._range_constraints.add(con) + if not is_constant(lhs_expr) or not is_constant(rhs_expr): + mutable_range_constant = _MutableRangeConstant() + mutable_range_constant.lhs_expr = lhs_expr + mutable_range_constant.rhs_expr = rhs_expr + mutable_range_constant.con = gurobipy_con + mutable_range_constant.slack_name = 'Rg' + conname + mutable_range_constant.gurobi_model = self._solver_model + self._mutable_helpers[con] = [mutable_range_constant] + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + for tmp in mutable_linear_coefficients: + tmp.con = gurobipy_con + tmp.gurobi_model = self._solver_model + if len(mutable_linear_coefficients) > 0: + if con not in self._mutable_helpers: + self._mutable_helpers[con] = mutable_linear_coefficients + else: + self._mutable_helpers[con].extend(mutable_linear_coefficients) + elif gurobi_expr.__class__ is gurobipy.QuadExpr: + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname + ) + elif con.has_lb() and con.has_ub(): + raise NotImplementedError( + 'Quadratic range constraints are not supported' + ) + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname + ) + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname + ) + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + if ( + len(mutable_linear_coefficients) > 0 + or len(mutable_quadratic_coefficients) > 0 + or not is_constant(repn_constant) + ): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_quadratic_constraint = _MutableQuadraticConstraint( + self._solver_model, + gurobipy_con, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + self._mutable_quadratic_helpers[con] = mutable_quadratic_constraint + else: + raise ValueError( + 'Unrecognized Gurobi expression type: ' + str(gurobi_expr.__class__) + ) + + self._pyomo_con_to_solver_con_map[con] = gurobipy_con + self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con + self._constraints_added_since_update.update(cons) + self._needs_updated = True + + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + conname = self._symbol_map.getSymbol(con, self._labeler) + level = con.level + if level == 1: + sos_type = gurobipy.GRB.SOS_TYPE1 + elif level == 2: + sos_type = gurobipy.GRB.SOS_TYPE2 + else: + raise ValueError( + "Solver does not support SOS level {0} constraints".format(level) + ) + + gurobi_vars = [] + weights = [] + + for v, w in con.get_items(): + v_id = id(v) + gurobi_vars.append(self._pyomo_var_to_solver_var_map[v_id]) + weights.append(w) + + gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) + self._pyomo_sos_to_solver_sos_map[con] = gurobipy_con + self._constraints_added_since_update.update(cons) + self._needs_updated = True + + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + for con in cons: + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_con = self._pyomo_con_to_solver_con_map[con] + self._solver_model.remove(solver_con) + self._symbol_map.removeSymbol(con) + del self._pyomo_con_to_solver_con_map[con] + del self._solver_con_to_pyomo_con_map[id(solver_con)] + self._range_constraints.discard(con) + self._mutable_helpers.pop(con, None) + self._mutable_quadratic_helpers.pop(con, None) + self._needs_updated = True + + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_sos_con = self._pyomo_sos_to_solver_sos_map[con] + self._solver_model.remove(solver_sos_con) + self._symbol_map.removeSymbol(con) + del self._pyomo_sos_to_solver_sos_map[con] + self._needs_updated = True + + def _remove_variables(self, variables: List[_GeneralVarData]): + for var in variables: + v_id = id(var) + if var in self._vars_added_since_update: + self._update_gurobi_model() + solver_var = self._pyomo_var_to_solver_var_map[v_id] + self._solver_model.remove(solver_var) + self._symbol_map.removeSymbol(var) + del self._pyomo_var_to_solver_var_map[v_id] + self._mutable_bounds.pop(v_id, None) + self._needs_updated = True + + def _remove_params(self, params: List[_ParamData]): + pass + + def _update_variables(self, variables: List[_GeneralVarData]): + for var in variables: + var_id = id(var) + if var_id not in self._pyomo_var_to_solver_var_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + self._mutable_bounds.pop((var_id, 'lb'), None) + self._mutable_bounds.pop((var_id, 'ub'), None) + gurobipy_var = self._pyomo_var_to_solver_var_map[var_id] + lb, ub, vtype = self._process_domain_and_bounds( + var, var_id, None, None, None, gurobipy_var + ) + gurobipy_var.setAttr('lb', lb) + gurobipy_var.setAttr('ub', ub) + gurobipy_var.setAttr('vtype', vtype) + self._needs_updated = True + + def update_params(self): + for con, helpers in self._mutable_helpers.items(): + for helper in helpers: + helper.update() + for k, (v, helper) in self._mutable_bounds.items(): + helper.update() + + for con, helper in self._mutable_quadratic_helpers.items(): + if con in self._constraints_added_since_update: + self._update_gurobi_model() + gurobi_con = helper.con + new_gurobi_expr = helper.get_updated_expression() + new_rhs = helper.get_updated_rhs() + new_sense = gurobi_con.qcsense + pyomo_con = self._solver_con_to_pyomo_con_map[id(gurobi_con)] + name = self._symbol_map.getSymbol(pyomo_con, self._labeler) + self._solver_model.remove(gurobi_con) + new_con = self._solver_model.addQConstr( + new_gurobi_expr, new_sense, new_rhs, name=name + ) + self._pyomo_con_to_solver_con_map[id(pyomo_con)] = new_con + del self._solver_con_to_pyomo_con_map[id(gurobi_con)] + self._solver_con_to_pyomo_con_map[id(new_con)] = pyomo_con + helper.con = new_con + self._constraints_added_since_update.add(con) + + helper = self._mutable_objective + pyomo_obj = self._objective + new_gurobi_expr = helper.get_updated_expression() + if new_gurobi_expr is not None: + if pyomo_obj.sense == minimize: + sense = gurobipy.GRB.MINIMIZE + else: + sense = gurobipy.GRB.MAXIMIZE + self._solver_model.setObjective(new_gurobi_expr, sense=sense) + + def _set_objective(self, obj): + if obj is None: + sense = gurobipy.GRB.MINIMIZE + gurobi_expr = 0 + repn_constant = 0 + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + else: + if obj.sense == minimize: + sense = gurobipy.GRB.MINIMIZE + elif obj.sense == maximize: + sense = gurobipy.GRB.MAXIMIZE + else: + raise ValueError( + 'Objective sense is not recognized: {0}'.format(obj.sense) + ) + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(obj.expr) + + mutable_constant = _MutableConstant() + mutable_constant.expr = repn_constant + mutable_objective = _MutableObjective( + self._solver_model, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + self._mutable_objective = mutable_objective + + # These two lines are needed as a workaround + # see PR #2454 + self._solver_model.setObjective(0) + self._solver_model.update() + + self._solver_model.setObjective(gurobi_expr + value(repn_constant), sense=sense) + self._needs_updated = True + + def _postsolve(self, timer: HierarchicalTimer): + config = self._config + + gprob = self._solver_model + grb = gurobipy.GRB + status = gprob.Status + + results = Results() + results.solution_loader = GurobiSolutionLoader(self) + results.timing_info.gurobi_time = gprob.Runtime + + if gprob.SolCount > 0: + if status == grb.OPTIMAL: + results.solution_status = SolutionStatus.optimal + else: + results.solution_status = SolutionStatus.feasible + else: + results.solution_status = SolutionStatus.noSolution + + if status == grb.LOADED: # problem is loaded, but no solution + results.termination_condition = TerminationCondition.unknown + elif status == grb.OPTIMAL: # optimal + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif status == grb.INFEASIBLE: + results.termination_condition = TerminationCondition.provenInfeasible + elif status == grb.INF_OR_UNBD: + results.termination_condition = TerminationCondition.infeasibleOrUnbounded + elif status == grb.UNBOUNDED: + results.termination_condition = TerminationCondition.unbounded + elif status == grb.CUTOFF: + results.termination_condition = TerminationCondition.objectiveLimit + elif status == grb.ITERATION_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.NODE_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.TIME_LIMIT: + results.termination_condition = TerminationCondition.maxTimeLimit + elif status == grb.SOLUTION_LIMIT: + results.termination_condition = TerminationCondition.unknown + elif status == grb.INTERRUPTED: + results.termination_condition = TerminationCondition.interrupted + elif status == grb.NUMERIC: + results.termination_condition = TerminationCondition.unknown + elif status == grb.SUBOPTIMAL: + results.termination_condition = TerminationCondition.unknown + elif status == grb.USER_OBJ_LIMIT: + results.termination_condition = TerminationCondition.objectiveLimit + else: + results.termination_condition = TerminationCondition.unknown + + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied and config.raise_exception_on_nonoptimal_result: + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) + + results.incumbent_objective = None + results.objective_bound = None + if self._objective is not None: + try: + results.incumbent_objective = gprob.ObjVal + except (gurobipy.GurobiError, AttributeError): + results.incumbent_objective = None + try: + results.objective_bound = gprob.ObjBound + except (gurobipy.GurobiError, AttributeError): + if self._objective.sense == minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective + ): + results.incumbent_objective = None + + results.iteration_count = gprob.getAttr('IterCount') + + timer.start('load solution') + if config.load_solutions: + if gprob.SolCount > 0: + self._load_vars() + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False and check ' + 'results.solution_status and ' + 'results.incumbent_objective before loading a solution.' + ) + timer.stop('load solution') + + return results + + def _load_suboptimal_mip_solution(self, vars_to_load, solution_number): + if ( + self.get_model_attr('NumIntVars') == 0 + and self.get_model_attr('NumBinVars') == 0 + ): + raise ValueError( + 'Cannot obtain suboptimal solutions for a continuous model' + ) + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + original_solution_number = self.get_gurobi_param_info('SolutionNumber')[2] + self.set_gurobi_param('SolutionNumber', solution_number) + gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] + vals = self._solver_model.getAttr("Xn", gurobi_vars_to_load) + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + self.set_gurobi_param('SolutionNumber', original_solution_number) + return res + + def _load_vars(self, vars_to_load=None, solution_number=0): + for v, val in self._get_primals( + vars_to_load=vars_to_load, solution_number=solution_number + ).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def _get_primals(self, vars_to_load=None, solution_number=0): + if self._needs_updated: + self._update_gurobi_model() # this is needed to ensure that solutions cannot be loaded after the model has been changed + + if self._solver_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = self._pyomo_var_to_solver_var_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + if solution_number != 0: + return self._load_suboptimal_mip_solution( + vars_to_load=vars_to_load, solution_number=solution_number + ) + else: + gurobi_vars_to_load = [ + var_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + vals = self._solver_model.getAttr("X", gurobi_vars_to_load) + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def _get_reduced_costs(self, vars_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + + if self._solver_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid reduced costs. Please ' + 'check the termination condition.' + ) + + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + res = ComponentMap() + if vars_to_load is None: + vars_to_load = self._pyomo_var_to_solver_var_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + gurobi_vars_to_load = [var_map[pyomo_var_id] for pyomo_var_id in vars_to_load] + vals = self._solver_model.getAttr("Rc", gurobi_vars_to_load) + + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + + return res + + def _get_duals(self, cons_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + + if self._solver_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid duals. Please ' + 'check the termination condition.' + ) + + con_map = self._pyomo_con_to_solver_con_map + reverse_con_map = self._solver_con_to_pyomo_con_map + dual = dict() + + if cons_to_load is None: + linear_cons_to_load = self._solver_model.getConstrs() + quadratic_cons_to_load = self._solver_model.getQConstrs() + else: + gurobi_cons_to_load = OrderedSet( + [con_map[pyomo_con] for pyomo_con in cons_to_load] + ) + linear_cons_to_load = list( + gurobi_cons_to_load.intersection( + OrderedSet(self._solver_model.getConstrs()) + ) + ) + quadratic_cons_to_load = list( + gurobi_cons_to_load.intersection( + OrderedSet(self._solver_model.getQConstrs()) + ) + ) + linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) + quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load) + + for gurobi_con, val in zip(linear_cons_to_load, linear_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + + return dual + + def update(self, timer: HierarchicalTimer = None): + if self._needs_updated: + self._update_gurobi_model() + super(Gurobi, self).update(timer=timer) + self._update_gurobi_model() + + def _update_gurobi_model(self): + self._solver_model.update() + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def get_model_attr(self, attr): + """ + Get the value of an attribute on the Gurobi model. + + Parameters + ---------- + attr: str + The attribute to get. See Gurobi documentation for descriptions of the attributes. + """ + if self._needs_updated: + self._update_gurobi_model() + return self._solver_model.getAttr(attr) + + def write(self, filename): + """ + Write the model to a file (e.g., and lp file). + + Parameters + ---------- + filename: str + Name of the file to which the model should be written. + """ + self._solver_model.write(filename) + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def set_linear_constraint_attr(self, con, attr, val): + """ + Set the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be modified. + attr: str + The attribute to be modified. Options are: + CBasis + DStart + Lazy + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'Sense', 'RHS', 'ConstrName'}: + raise ValueError( + 'Linear constraint attr {0} cannot be set with' + + ' the set_linear_constraint_attr method. Please use' + + ' the remove_constraint and add_constraint methods.'.format(attr) + ) + self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) + self._needs_updated = True + + def set_var_attr(self, var, attr, val): + """ + Set the value of an attribute on a gurobi variable. + + Parameters + ---------- + var: pyomo.core.base.var._GeneralVarData + The pyomo var for which the corresponding gurobi var attribute + should be modified. + attr: str + The attribute to be modified. Options are: + Start + VarHintVal + VarHintPri + BranchPriority + VBasis + PStart + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'LB', 'UB', 'VType', 'VarName'}: + raise ValueError( + 'Var attr {0} cannot be set with' + + ' the set_var_attr method. Please use' + + ' the update_var method.'.format(attr) + ) + if attr == 'Obj': + raise ValueError( + 'Var attr Obj cannot be set with' + + ' the set_var_attr method. Please use' + + ' the set_objective method.' + ) + self._pyomo_var_to_solver_var_map[id(var)].setAttr(attr, val) + self._needs_updated = True + + def get_var_attr(self, var, attr): + """ + Get the value of an attribute on a gurobi var. + + Parameters + ---------- + var: pyomo.core.base.var._GeneralVarData + The pyomo var for which the corresponding gurobi var attribute + should be retrieved. + attr: str + The attribute to get. See gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_var_to_solver_var_map[id(var)].getAttr(attr) + + def get_linear_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def get_sos_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi sos constraint. + + Parameters + ---------- + con: pyomo.core.base.sos._SOSConstraintData + The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_sos_to_solver_sos_map[con].getAttr(attr) + + def get_quadratic_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi quadratic constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def set_gurobi_param(self, param, val): + """ + Set a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to set. Options include any gurobi parameter. + Please see the Gurobi documentation for options. + val: any + The value to set the parameter to. See Gurobi documentation for possible values. + """ + self._solver_model.setParam(param, val) + + def get_gurobi_param_info(self, param): + """ + Get information about a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to get info for. See Gurobi documentation for possible options. + + Returns + ------- + six-tuple containing the parameter name, type, value, minimum value, maximum value, and default value. + """ + return self._solver_model.getParamInfo(param) + + def _intermediate_callback(self): + def f(gurobi_model, where): + self._callback_func(self._model, self, where) + + return f + + def set_callback(self, func=None): + """ + Specify a callback for gurobi to use. + + Parameters + ---------- + func: function + The function to call. The function should have three arguments. The first will be the pyomo model being + solved. The second will be the GurobiPersistent instance. The third will be an enum member of + gurobipy.GRB.Callback. This will indicate where in the branch and bound algorithm gurobi is at. For + example, suppose we want to solve + + .. math:: + + min 2*x + y + + s.t. + + y >= (x-2)**2 + + 0 <= x <= 4 + + y >= 0 + + y integer + + as an MILP using extended cutting planes in callbacks. + + >>> from gurobipy import GRB # doctest:+SKIP + >>> import pyomo.environ as pe + >>> from pyomo.core.expr.taylor_series import taylor_series_expansion + >>> from pyomo.contrib import appsi + >>> + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var(bounds=(0, 4)) + >>> m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + >>> m.obj = pe.Objective(expr=2*m.x + m.y) + >>> m.cons = pe.ConstraintList() # for the cutting planes + >>> + >>> def _add_cut(xval): + ... # a function to generate the cut + ... m.x.value = xval + ... return m.cons.add(m.y >= taylor_series_expansion((m.x - 2)**2)) + ... + >>> _c = _add_cut(0) # start with 2 cuts at the bounds of x + >>> _c = _add_cut(4) # this is an arbitrary choice + >>> + >>> opt = appsi.solvers.Gurobi() + >>> opt.config.stream_solver = True + >>> opt.set_instance(m) # doctest:+SKIP + >>> opt.gurobi_options['PreCrush'] = 1 + >>> opt.gurobi_options['LazyConstraints'] = 1 + >>> + >>> def my_callback(cb_m, cb_opt, cb_where): + ... if cb_where == GRB.Callback.MIPSOL: + ... cb_opt.cbGetSolution(vars=[m.x, m.y]) + ... if m.y.value < (m.x.value - 2)**2 - 1e-6: + ... cb_opt.cbLazy(_add_cut(m.x.value)) + ... + >>> opt.set_callback(my_callback) + >>> res = opt.solve(m) # doctest:+SKIP + + """ + if func is not None: + self._callback_func = func + self._callback = self._intermediate_callback() + else: + self._callback = None + self._callback_func = None + + def cbCut(self, con): + """ + Add a cut within a callback. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The cut to add + """ + if not con.active: + raise ValueError('cbCut expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbCut expected a non-trivial constraint') + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbCut.') + if not is_fixed(con.lower): + raise ValueError( + 'Lower bound of constraint {0} is not constant.'.format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + 'Upper bound of constraint {0} is not constant.'.format(con) + ) + + if con.equality: + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper - repn_constant), + ) + else: + raise ValueError( + 'Constraint does not have a lower or an upper bound {0} \n'.format(con) + ) + + def cbGet(self, what): + return self._solver_model.cbGet(what) + + def cbGetNodeRel(self, vars): + """ + Parameters + ---------- + vars: Var or iterable of Var + """ + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetNodeRel(gurobi_vars) + for i, v in enumerate(vars): + v.set_value(var_values[i], skip_validation=True) + + def cbGetSolution(self, vars): + """ + Parameters + ---------- + vars: iterable of vars + """ + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetSolution(gurobi_vars) + for i, v in enumerate(vars): + v.set_value(var_values[i], skip_validation=True) + + def cbLazy(self, con): + """ + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The lazy constraint to add + """ + if not con.active: + raise ValueError('cbLazy expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbLazy expected a non-trivial constraint') + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbLazy.') + if not is_fixed(con.lower): + raise ValueError( + 'Lower bound of constraint {0} is not constant.'.format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + 'Upper bound of constraint {0} is not constant.'.format(con) + ) + + if con.equality: + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper - repn_constant), + ) + else: + raise ValueError( + 'Constraint does not have a lower or an upper bound {0} \n'.format(con) + ) + + def cbSetSolution(self, vars, solution): + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + self._solver_model.cbSetSolution(gurobi_vars, solution) + + def cbUseSolution(self): + return self._solver_model.cbUseSolution() + + def reset(self): + self._solver_model.reset() diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index 54d03eaf74b..e66818482b4 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -12,9 +12,11 @@ from .factory import SolverFactory from .ipopt import ipopt +from .gurobi import Gurobi def load(): SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( ipopt ) + SolverFactory.register(name='gurobi_v2', doc='New interface to Gurobi')(Gurobi) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 68654a4e9d7..a2e4d90b898 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -122,7 +122,7 @@ def parse_sol_file( # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? result.solution_status = SolutionStatus.infeasible - result.termination_condition = TerminationCondition.iterationLimit + result.termination_condition = TerminationCondition.iterationLimit # this is not always correct elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " @@ -205,4 +205,4 @@ def parse_sol_file( ) line = sol_file.readline() - return result, sol_data + return result, sol_data diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py new file mode 100644 index 00000000000..f53088506f9 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -0,0 +1,691 @@ +from pyomo.common.errors import PyomoException +import pyomo.common.unittest as unittest +import pyomo.environ as pe +from pyomo.contrib.solver.gurobi import Gurobi +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus +from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.core.expr.taylor_series import taylor_series_expansion + + +opt = Gurobi() +if not opt.available(): + raise unittest.SkipTest +import gurobipy + + +def create_pmedian_model(): + d_dict = { + (1, 1): 1.777356642700564, + (1, 2): 1.6698255595592497, + (1, 3): 1.099139603924817, + (1, 4): 1.3529705111901453, + (1, 5): 1.467907742900842, + (1, 6): 1.5346837414708774, + (2, 1): 1.9783090609123972, + (2, 2): 1.130315350158659, + (2, 3): 1.6712434682302661, + (2, 4): 1.3642294159473756, + (2, 5): 1.4888357071619858, + (2, 6): 1.2030122107340537, + (3, 1): 1.6661983755713592, + (3, 2): 1.227663031206932, + (3, 3): 1.4580640582967632, + (3, 4): 1.0407223975549575, + (3, 5): 1.9742897953778287, + (3, 6): 1.4874760742689066, + (4, 1): 1.4616138636373597, + (4, 2): 1.7141471558082002, + (4, 3): 1.4157281494999725, + (4, 4): 1.888011688001529, + (4, 5): 1.0232934487237717, + (4, 6): 1.8335062677845464, + (5, 1): 1.468494740997508, + (5, 2): 1.8114798126442795, + (5, 3): 1.9455914886158723, + (5, 4): 1.983088378194899, + (5, 5): 1.1761820755785306, + (5, 6): 1.698655759576308, + (6, 1): 1.108855711312383, + (6, 2): 1.1602637342062019, + (6, 3): 1.0928602740245892, + (6, 4): 1.3140620798928404, + (6, 5): 1.0165386843386672, + (6, 6): 1.854049125736362, + (7, 1): 1.2910160386456968, + (7, 2): 1.7800475863350327, + (7, 3): 1.5480965161255695, + (7, 4): 1.1943306766997612, + (7, 5): 1.2920382721805297, + (7, 6): 1.3194527773994338, + (8, 1): 1.6585982235379078, + (8, 2): 1.2315210354122292, + (8, 3): 1.6194303369953538, + (8, 4): 1.8953386098022103, + (8, 5): 1.8694342085696831, + (8, 6): 1.2938069356684523, + (9, 1): 1.4582048085805495, + (9, 2): 1.484979797871119, + (9, 3): 1.2803882693587225, + (9, 4): 1.3289569463506004, + (9, 5): 1.9842424240265042, + (9, 6): 1.0119441379208745, + (10, 1): 1.1429007682932852, + (10, 2): 1.6519772165446711, + (10, 3): 1.0749931799469326, + (10, 4): 1.2920787022811089, + (10, 5): 1.7934429721917704, + (10, 6): 1.9115931008709737, + } + + model = pe.ConcreteModel() + model.N = pe.Param(initialize=10) + model.Locations = pe.RangeSet(1, model.N) + model.P = pe.Param(initialize=3) + model.M = pe.Param(initialize=6) + model.Customers = pe.RangeSet(1, model.M) + model.d = pe.Param( + model.Locations, model.Customers, initialize=d_dict, within=pe.Reals + ) + model.x = pe.Var(model.Locations, model.Customers, bounds=(0.0, 1.0)) + model.y = pe.Var(model.Locations, within=pe.Binary) + + def rule(model): + return sum( + model.d[n, m] * model.x[n, m] + for n in model.Locations + for m in model.Customers + ) + + model.obj = pe.Objective(rule=rule) + + def rule(model, m): + return (sum(model.x[n, m] for n in model.Locations), 1.0) + + model.single_x = pe.Constraint(model.Customers, rule=rule) + + def rule(model, n, m): + return (None, model.x[n, m] - model.y[n], 0.0) + + model.bound_y = pe.Constraint(model.Locations, model.Customers, rule=rule) + + def rule(model): + return (sum(model.y[n] for n in model.Locations) - model.P, 0.0) + + model.num_facilities = pe.Constraint(rule=rule) + + return model + + +class TestGurobiPersistentSimpleLPUpdates(unittest.TestCase): + def setUp(self): + self.m = pe.ConcreteModel() + m = self.m + m.x = pe.Var() + m.y = pe.Var() + m.p1 = pe.Param(mutable=True) + m.p2 = pe.Param(mutable=True) + m.p3 = pe.Param(mutable=True) + m.p4 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.x + m.y) + m.c1 = pe.Constraint(expr=m.y - m.p1 * m.x >= m.p2) + m.c2 = pe.Constraint(expr=m.y - m.p3 * m.x >= m.p4) + + def get_solution(self): + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + p1 = self.m.p1.value + p2 = self.m.p2.value + p3 = self.m.p3.value + p4 = self.m.p4.value + A = np.array([[1, -p1], [1, -p3]]) + rhs = np.array([p2, p4]) + sol = np.linalg.solve(A, rhs) + x = float(sol[1]) + y = float(sol[0]) + return x, y + + def set_params(self, p1, p2, p3, p4): + self.m.p1.value = p1 + self.m.p2.value = p2 + self.m.p3.value = p3 + self.m.p4.value = p4 + + def test_lp(self): + self.set_params(-1, -2, 0.1, -2) + x, y = self.get_solution() + opt = Gurobi() + res = opt.solve(self.m) + self.assertAlmostEqual(x + y, res.incumbent_objective) + self.assertAlmostEqual(x + y, res.objective_bound) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertTrue(res.incumbent_objective is not None) + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + + self.set_params(-1.25, -1, 0.5, -2) + opt.config.load_solutions = False + res = opt.solve(self.m) + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + x, y = self.get_solution() + self.assertNotAlmostEqual(x, self.m.x.value) + self.assertNotAlmostEqual(y, self.m.y.value) + res.solution_loader.load_vars() + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + + +class TestGurobiPersistent(unittest.TestCase): + def test_nonconvex_qcp_objective_bound_1(self): + # the goal of this test is to ensure we can get an objective bound + # for nonconvex but continuous problems even if a feasible solution + # is not found + # + # This is a fragile test because it could fail if Gurobi's algorithms improve + # (e.g., a heuristic solution is found before an objective bound of -8 is reached + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-5, 5)) + m.y = pe.Var(bounds=(-5, 5)) + m.obj = pe.Objective(expr=-m.x**2 - m.y) + m.c1 = pe.Constraint(expr=m.y <= -2 * m.x + 1) + m.c2 = pe.Constraint(expr=m.y <= m.x - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.config.solver_options['BestBdStop'] = -8 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertEqual(res.incumbent_objective, None) + self.assertAlmostEqual(res.objective_bound, -8) + + def test_nonconvex_qcp_objective_bound_2(self): + # the goal of this test is to ensure we can objective_bound properly + # for nonconvex but continuous problems when the solver terminates with a nonzero gap + # + # This is a fragile test because it could fail if Gurobi's algorithms change + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-5, 5)) + m.y = pe.Var(bounds=(-5, 5)) + m.obj = pe.Objective(expr=-m.x**2 - m.y) + m.c1 = pe.Constraint(expr=m.y <= -2 * m.x + 1) + m.c2 = pe.Constraint(expr=m.y <= m.x - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.config.solver_options['MIPGap'] = 0.5 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -4) + self.assertAlmostEqual(res.objective_bound, -6) + + def test_range_constraints(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.xl = pe.Param(initialize=-1, mutable=True) + m.xu = pe.Param(initialize=1, mutable=True) + m.c = pe.Constraint(expr=pe.inequality(m.xl, m.x, m.xu)) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + opt.set_instance(m) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -1) + + m.xl.value = -3 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -3) + + del m.obj + m.obj = pe.Objective(expr=m.x, sense=pe.maximize) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + + m.xu.value = 3 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 3) + + def test_quadratic_constraint_with_params(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.con = pe.Constraint(expr=m.y >= m.a * m.x**2 + m.b * m.x + m.c) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + m.a.value = 2 + m.b.value = 4 + m.c.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + def test_quadratic_objective(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.obj = pe.Objective(expr=m.a * m.x**2 + m.b * m.x + m.c) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + res.incumbent_objective, + m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, + ) + + m.a.value = 2 + m.b.value = 4 + m.c.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + res.incumbent_objective, + m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, + ) + + def test_var_bounds(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -1) + + m.x.setlb(-3) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -3) + + del m.obj + m.obj = pe.Objective(expr=m.x, sense=pe.maximize) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + + m.x.setub(3) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 3) + + def test_fixed_var(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.con = pe.Constraint(expr=m.y >= m.a * m.x**2 + m.b * m.x + m.c) + + m.x.fix(1) + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 3) + + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 7) + + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + def test_linear_constraint_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c = pe.Constraint(expr=m.x + m.y == 1) + + opt = Gurobi() + opt.set_instance(m) + opt.set_linear_constraint_attr(m.c, 'Lazy', 1) + self.assertEqual(opt.get_linear_constraint_attr(m.c, 'Lazy'), 1) + + def test_quadratic_constraint_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c = pe.Constraint(expr=m.y >= m.x**2) + + opt = Gurobi() + opt.set_instance(m) + self.assertEqual(opt.get_quadratic_constraint_attr(m.c, 'QCRHS'), 0) + + def test_var_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + opt.set_instance(m) + opt.set_var_attr(m.x, 'Start', 1) + self.assertEqual(opt.get_var_attr(m.x, 'Start'), 1) + + def test_callback(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0, 4)) + m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + m.obj = pe.Objective(expr=2 * m.x + m.y) + m.cons = pe.ConstraintList() + + def _add_cut(xval): + m.x.value = xval + return m.cons.add(m.y >= taylor_series_expansion((m.x - 2) ** 2)) + + _add_cut(0) + _add_cut(4) + + opt = Gurobi() + opt.set_instance(m) + opt.set_gurobi_param('PreCrush', 1) + opt.set_gurobi_param('LazyConstraints', 1) + + def _my_callback(cb_m, cb_opt, cb_where): + if cb_where == gurobipy.GRB.Callback.MIPSOL: + cb_opt.cbGetSolution(vars=[m.x, m.y]) + if m.y.value < (m.x.value - 2) ** 2 - 1e-6: + cb_opt.cbLazy(_add_cut(m.x.value)) + + opt.set_callback(_my_callback) + opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + + def test_nonconvex(self): + if gurobipy.GRB.VERSION_MAJOR < 9: + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c = pe.Constraint(expr=m.y == (m.x - 1) ** 2 - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.3660254037844423, 2) + self.assertAlmostEqual(m.y.value, -0.13397459621555508, 2) + + def test_nonconvex2(self): + if gurobipy.GRB.VERSION_MAJOR < 9: + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=0 <= -m.y + (m.x - 1) ** 2 - 2) + m.c2 = pe.Constraint(expr=0 >= -m.y + (m.x - 1) ** 2 - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.3660254037844423, 2) + self.assertAlmostEqual(m.y.value, -0.13397459621555508, 2) + + def test_solution_number(self): + m = create_pmedian_model() + opt = Gurobi() + opt.config.solver_options['PoolSolutions'] = 3 + opt.config.solver_options['PoolSearchMode'] = 2 + res = opt.solve(m) + num_solutions = opt.get_model_attr('SolCount') + self.assertEqual(num_solutions, 3) + res.solution_loader.load_vars(solution_number=0) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.431184939357673) + res.solution_loader.load_vars(solution_number=1) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.584793218502477) + res.solution_loader.load_vars(solution_number=2) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.592304628123309) + + def test_zero_time_limit(self): + m = create_pmedian_model() + opt = Gurobi() + opt.config.time_limit = 0 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + num_solutions = opt.get_model_attr('SolCount') + + # Behavior is different on different platforms, so + # we have to see if there are any solutions + # This means that there is no guarantee we are testing + # what we are trying to test. Unfortunately, I'm + # not sure of a good way to guarantee that + if num_solutions == 0: + self.assertIsNone(res.incumbent_objective) + + +class TestManualModel(unittest.TestCase): + def setUp(self): + opt = Gurobi() + opt.config.auto_updates.check_for_new_or_removed_params = False + opt.config.auto_updates.check_for_new_or_removed_vars = False + opt.config.auto_updates.check_for_new_or_removed_constraints = False + opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_vars = False + opt.config.auto_updates.update_constraints = False + opt.config.auto_updates.update_named_expressions = False + self.opt = opt + + def test_basics(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y >= 2 * m.x + 1) + + opt = self.opt + opt.set_instance(m) + + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -10) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 10) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -0.4) + + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + opt.add_constraints([m.c2]) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 2) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + opt.config.load_solutions = False + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt.remove_constraints([m.c2]) + m.del_component(m.c2) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + self.assertEqual(opt.get_gurobi_param_info('FeasibilityTol')[2], 1e-6) + opt.config.solver_options['FeasibilityTol'] = 1e-7 + opt.config.load_solutions = True + res = opt.solve(m) + self.assertEqual(opt.get_gurobi_param_info('FeasibilityTol')[2], 1e-7) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + + m.x.setlb(-5) + m.x.setub(5) + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -5) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 5) + + m.x.fix(0) + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), 0) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 0) + + m.x.unfix() + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -5) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 5) + + m.c2 = pe.Constraint(expr=m.y >= m.x**2) + opt.add_constraints([m.c2]) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 1) + + opt.remove_constraints([m.c2]) + m.del_component(m.c2) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + m.z = pe.Var() + opt.add_variables([m.z]) + self.assertEqual(opt.get_model_attr('NumVars'), 3) + opt.remove_variables([m.z]) + del m.z + self.assertEqual(opt.get_model_attr('NumVars'), 2) + + def test_update1(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x**2 + m.y**2) + + opt = self.opt + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + opt.remove_constraints([m.c1]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 0) + + opt.add_constraints([m.c1]) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + def test_update2(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c2 = pe.Constraint(expr=m.x + m.y == 1) + + opt = self.opt + opt.config.symbolic_solver_labels = True + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 0) + + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + def test_update3(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x**2 + m.y**2) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + m.c2 = pe.Constraint(expr=m.y >= m.x**2) + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + def test_update4(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x + m.y) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + m.c2 = pe.Constraint(expr=m.y >= m.x) + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + def test_update5(self): + m = pe.ConcreteModel() + m.a = pe.Set(initialize=[1, 2, 3], ordered=True) + m.x = pe.Var(m.a, within=pe.Binary) + m.y = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.SOSConstraint(var=m.x, sos=1) + + opt = self.opt + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + + opt.remove_sos_constraints([m.c1]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 0) + + opt.add_sos_constraints([m.c1]) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + + def test_update6(self): + m = pe.ConcreteModel() + m.a = pe.Set(initialize=[1, 2, 3], ordered=True) + m.x = pe.Var(m.a, within=pe.Binary) + m.y = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.SOSConstraint(var=m.x, sos=1) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + m.c2 = pe.SOSConstraint(var=m.x, sos=2) + opt.add_sos_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + opt.remove_sos_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py new file mode 100644 index 00000000000..0499f1abb5d --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -0,0 +1,1350 @@ +import pyomo.environ as pe +from pyomo.common.dependencies import attempt_import +import pyomo.common.unittest as unittest + +parameterized, param_available = attempt_import('parameterized') +parameterized = parameterized.parameterized +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.ipopt import ipopt +from pyomo.contrib.solver.gurobi import Gurobi +from typing import Type +from pyomo.core.expr.numeric_expr import LinearExpression +import os +import math + +numpy, numpy_available = attempt_import('numpy') +import random +from pyomo import gdp + + +if not param_available: + raise unittest.SkipTest('Parameterized is not available.') + +all_solvers = [ + ('gurobi', Gurobi), + ('ipopt', ipopt), +] +mip_solvers = [('gurobi', Gurobi)] +nlp_solvers = [('ipopt', ipopt)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] +miqcqp_solvers = [('gurobi', Gurobi)] + + +def _load_tests(solver_list): + res = list() + for solver_name, solver in solver_list: + test_name = f"{solver_name}" + res.append((test_name, solver)) + return res + + +@unittest.skipUnless(numpy_available, 'numpy is not available') +class TestSolvers(unittest.TestCase): + @parameterized.expand(input=_load_tests(all_solvers)) + def test_remove_variable_and_objective( + self, name: str, opt_class: Type[SolverBase], + ): + # this test is for issue #2888 + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(2, None)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 2) + + del m.x + del m.obj + m.x = pe.Var(bounds=(2, None)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_stale_vars( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y >= -m.x) + m.x.value = 1 + m.y.value = 1 + m.z.value = 1 + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertFalse(m.z.stale) + + res = opt.solve(m) + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertTrue(m.z.stale) + + opt.config.load_solutions = False + res = opt.solve(m) + self.assertTrue(m.x.stale) + self.assertTrue(m.y.stale) + self.assertTrue(m.z.stale) + res.solution_loader.load_vars() + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertTrue(m.z.stale) + + res = opt.solve(m) + self.assertTrue(m.x.stale) + self.assertTrue(m.y.stale) + self.assertTrue(m.z.stale) + res.solution_loader.load_vars([m.y]) + self.assertFalse(m.y.stale) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_range_constraint( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.obj = pe.Objective(expr=m.x) + m.c = pe.Constraint(expr=(-1, m.x, 1)) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c], 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_reduced_costs( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-2, 2)) + m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.y.value, -2) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_reduced_costs2( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 1) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_param_changes( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_immutable_param( + self, name: str, opt_class: Type[SolverBase], + ): + """ + This test is important because component_data_objects returns immutable params as floats. + We want to make sure we process these correctly. + """ + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(initialize=-1) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + + params_to_test = [(1, 2, 1), (1, 2, 1), (1, 3, 1)] + for a1, b1, b2 in params_to_test: + a2 = m.a2.value + m.a1.value = a1 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_equality( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + if isinstance(opt, ipopt): + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) + m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_linear_expression( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + e = LinearExpression( + constant=m.b1, linear_coefs=[-1, m.a1], linear_vars=[m.y, m.x] + ) + m.c1 = pe.Constraint(expr=e == 0) + e = LinearExpression( + constant=m.b2, linear_coefs=[-1, m.a2], linear_vars=[m.y, m.x] + ) + m.c2 = pe.Constraint(expr=e == 0) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_no_objective( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) + m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertEqual(res.incumbent_objective, None) + self.assertEqual(res.objective_bound, None) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_add_remove_cons( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + a1 = -1 + a2 = 1 + b1 = 1 + b2 = 2 + a3 = 1 + b3 = 3 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) + self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + + del m.c3 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_results_infeasible( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y <= m.x - 1) + with self.assertRaises(Exception): + res = opt.solve(m) + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertNotEqual(res.solution_status, SolutionStatus.optimal) + if isinstance(opt, ipopt): + acceptable_termination_conditions = { + TerminationCondition.locallyInfeasible, + TerminationCondition.unbounded, + } + else: + acceptable_termination_conditions = { + TerminationCondition.provenInfeasible, + TerminationCondition.infeasibleOrUnbounded, + } + self.assertIn(res.termination_condition, acceptable_termination_conditions) + self.assertAlmostEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, None) + self.assertTrue(res.incumbent_objective is None) + + if not isinstance(opt, ipopt): + # ipopt can return the values of the variables/duals at the last iterate + # even if it did not converge; raise_exception_on_nonoptimal_result + # is set to False, so we are free to load infeasible solutions + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_duals(self, name: str, opt_class: Type[SolverBase],): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y - m.x >= 0) + m.c2 = pe.Constraint(expr=m.y + m.x - 2 >= 0) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) + + duals = res.solution_loader.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) + + @parameterized.expand(input=_load_tests(qcp_solvers)) + def test_mutable_quadratic_coefficient( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=-1, mutable=True) + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c = pe.Constraint(expr=m.y >= (m.a * m.x + m.b) ** 2) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.41024548525899274, 4) + self.assertAlmostEqual(m.y.value, 0.34781038127030117, 4) + m.a.value = 2 + m.b.value = -0.5 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.10256137418973625, 4) + self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) + + @parameterized.expand(input=_load_tests(qcp_solvers)) + def test_mutable_quadratic_objective( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=-1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.d = pe.Param(initialize=1, mutable=True) + m.obj = pe.Objective(expr=m.x**2 + m.c * m.y**2 + m.d * m.x) + m.ccon = pe.Constraint(expr=m.y >= (m.a * m.x + m.b) ** 2) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.2719178742733325, 4) + self.assertAlmostEqual(m.y.value, 0.5301035741688002, 4) + m.c.value = 3.5 + m.d.value = -1 + res = opt.solve(m) + + self.assertAlmostEqual(m.x.value, 0.6962249634573562, 4) + self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars( + self, name: str, opt_class: Type[SolverBase], + ): + for treat_fixed_vars_as_params in [True, False]: + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = treat_fixed_vars_as_params + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.x.fix(0) + m.y = pe.Var() + a1 = 1 + a2 = -1 + b1 = 1 + b2 = 2 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 3) + m.x.value = 0 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars_2( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.x.fix(0) + m.y = pe.Var() + a1 = 1 + a2 = -1 + b1 = 1 + b2 = 2 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 3) + m.x.value = 0 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars_3( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x + m.y) + m.c1 = pe.Constraint(expr=m.x == 2 / m.y) + m.y.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_fixed_vars_4( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.x == 2 / m.y) + m.y.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + m.y.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2**0.5) + self.assertAlmostEqual(m.y.value, 2**0.5) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_mutable_param_with_range( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(initialize=0, mutable=True) + m.a2 = pe.Param(initialize=0, mutable=True) + m.b1 = pe.Param(initialize=0, mutable=True) + m.b2 = pe.Param(initialize=0, mutable=True) + m.c1 = pe.Param(initialize=0, mutable=True) + m.c2 = pe.Param(initialize=0, mutable=True) + m.obj = pe.Objective(expr=m.y) + m.con1 = pe.Constraint(expr=(m.b1, m.y - m.a1 * m.x, m.c1)) + m.con2 = pe.Constraint(expr=(m.b2, m.y - m.a2 * m.x, m.c2)) + + np.random.seed(0) + params_to_test = [ + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.minimize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.maximize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.minimize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.maximize, + ), + ] + for a1, a2, b1, b2, c1, c2, sense in params_to_test: + m.a1.value = float(a1) + m.a2.value = float(a2) + m.b1.value = float(b1) + m.b2.value = float(b2) + m.c1.value = float(c1) + m.c2.value = float(c2) + m.obj.sense = sense + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + if sense is pe.minimize: + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value + 1e-12) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + else: + self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) + self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound is None or res.objective_bound >= m.y.value - 1e-12) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_add_and_remove_vars( + self, name: str, opt_class: Type[SolverBase], + ): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.y = pe.Var(bounds=(-1, None)) + m.obj = pe.Objective(expr=m.y) + if opt.is_persistent(): + opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_vars = False + opt.config.auto_updates.update_constraints = False + opt.config.auto_updates.update_named_expressions = False + opt.config.auto_updates.check_for_new_or_removed_params = False + opt.config.auto_updates.check_for_new_or_removed_constraints = False + opt.config.auto_updates.check_for_new_or_removed_vars = False + opt.config.load_solutions = False + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.y.value, -1) + m.x = pe.Var() + a1 = 1 + a2 = -1 + b1 = 2 + b2 = 1 + m.c1 = pe.Constraint(expr=(0, m.y - a1 * m.x - b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + a2 * m.x + b2, 0)) + if opt.is_persistent(): + opt.add_constraints([m.c1, m.c2]) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.c1.deactivate() + m.c2.deactivate() + if opt.is_persistent(): + opt.remove_constraints([m.c1, m.c2]) + m.x.value = None + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, -1) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_exp(self, name: str, opt_class: Type[SolverBase],): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.42630274815985264) + self.assertAlmostEqual(m.y.value, 0.6529186341994245) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_log(self, name: str, opt_class: Type[SolverBase],): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(initialize=1) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.6529186341994245) + self.assertAlmostEqual(m.y.value, -0.42630274815985264) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_with_numpy( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + a1 = 1 + b1 = 3 + a2 = -2 + b2 = 1 + m.c1 = pe.Constraint( + expr=(numpy.float64(0), m.y - numpy.int64(1) * m.x - numpy.float32(3), None) + ) + m.c2 = pe.Constraint( + expr=( + None, + -m.y + numpy.int32(-2) * m.x + numpy.float64(1), + numpy.float16(0), + ) + ) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bounds_with_params( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.y = pe.Var() + m.p = pe.Param(mutable=True) + m.y.setlb(m.p) + m.p.value = 1 + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.p.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, -1) + m.y.setlb(None) + m.y.setub(m.p) + m.obj.sense = pe.maximize + m.p.value = 5 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 5) + m.p.value = 4 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 4) + m.y.setub(None) + m.y.setlb(m.p) + m.obj.sense = pe.minimize + m.p.value = 3 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 3) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_solution_loader( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1, None)) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.x, None)) + m.c2 = pe.Constraint(expr=(0, m.y - m.x + 1, None)) + opt.config.load_solutions = False + res = opt.solve(m) + self.assertIsNone(m.x.value) + self.assertIsNone(m.y.value) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + m.x.value = None + m.y.value = None + res.solution_loader.load_vars([m.y]) + self.assertAlmostEqual(m.y.value, 1) + primals = res.solution_loader.get_primals() + self.assertIn(m.x, primals) + self.assertIn(m.y, primals) + self.assertAlmostEqual(primals[m.x], 1) + self.assertAlmostEqual(primals[m.y], 1) + primals = res.solution_loader.get_primals([m.y]) + self.assertNotIn(m.x, primals) + self.assertIn(m.y, primals) + self.assertAlmostEqual(primals[m.y], 1) + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_time_limit( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + from sys import platform + + if platform == 'win32': + raise unittest.SkipTest + + N = 30 + m = pe.ConcreteModel() + m.jobs = pe.Set(initialize=list(range(N))) + m.tasks = pe.Set(initialize=list(range(N))) + m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) + + random.seed(0) + coefs = list() + lin_vars = list() + for j in m.jobs: + for t in m.tasks: + coefs.append(random.uniform(0, 10)) + lin_vars.append(m.x[j, t]) + obj_expr = LinearExpression( + linear_coefs=coefs, linear_vars=lin_vars, constant=0 + ) + m.obj = pe.Objective(expr=obj_expr, sense=pe.maximize) + + m.c1 = pe.Constraint(m.jobs) + m.c2 = pe.Constraint(m.tasks) + for j in m.jobs: + expr = LinearExpression( + linear_coefs=[1] * N, + linear_vars=[m.x[j, t] for t in m.tasks], + constant=0, + ) + m.c1[j] = expr == 1 + for t in m.tasks: + expr = LinearExpression( + linear_coefs=[1] * N, + linear_vars=[m.x[j, t] for j in m.jobs], + constant=0, + ) + m.c2[t] = expr == 1 + if isinstance(opt, ipopt): + opt.config.time_limit = 1e-6 + else: + opt.config.time_limit = 0 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertIn( + res.termination_condition, {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit} + ) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_objective_changes( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + del m.obj + m.obj = pe.Objective(expr=2 * m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + m.obj.expr = 3 * m.y + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + m.obj.sense = pe.maximize + opt.config.raise_exception_on_nonoptimal_result = False + opt.config.load_solutions = False + res = opt.solve(m) + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) + m.obj.sense = pe.minimize + opt.config.load_solutions = True + del m.obj + m.obj = pe.Objective(expr=m.x * m.y) + m.x.fix(2) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 6, 6) + m.x.fix(3) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 12, 6) + m.x.unfix() + m.y.fix(2) + m.x.setlb(-3) + m.x.setub(5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -2, 6) + m.y.unfix() + m.x.setlb(None) + m.x.setub(None) + m.e = pe.Expression(expr=2) + del m.obj + m.obj = pe.Objective(expr=m.e * m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + m.e.expr = 3 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + if opt.is_persistent(): + opt.config.auto_updates.check_for_new_objective = False + m.e.expr = 4 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_domain( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-1) + m.x.domain = pe.Reals + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -1) + m.x.domain = pe.NonNegativeReals + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + + @parameterized.expand(input=_load_tests(mip_solvers)) + def test_domain_with_integers( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(0.5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-5.5) + m.x.domain = pe.Integers + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -5) + m.x.domain = pe.Binary + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(0.5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_binaries( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(domain=pe.Binary) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y >= m.x) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = False + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + @parameterized.expand(input=_load_tests(mip_solvers)) + def test_with_gdp( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var(bounds=(-10, 10)) + m.obj = pe.Objective(expr=m.y) + m.d1 = gdp.Disjunct() + m.d1.c1 = pe.Constraint(expr=m.y >= m.x + 2) + m.d1.c2 = pe.Constraint(expr=m.y >= -m.x + 2) + m.d2 = gdp.Disjunct() + m.d2.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.d2.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.disjunction = gdp.Disjunction(expr=[m.d2, m.d1]) + pe.TransformationFactory("gdp.bigm").apply_to(m) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt: SolverBase = opt_class() + opt.use_extensions = True + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + @parameterized.expand(input=all_solvers) + def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.b = pe.Block() + m.b.obj = pe.Objective(expr=m.y) + m.b.c1 = pe.Constraint(expr=m.y >= m.x + 2) + m.b.c2 = pe.Constraint(expr=m.y >= -m.x) + + res = opt.solve(m.b) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.y.value, 1) + + m.x.setlb(0) + res = opt.solve(m.b) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=all_solvers) + def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y >= -m.x) + m.c3 = pe.Constraint(expr=m.y >= m.z + 1) + m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) + + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 1) + sol = res.solution_loader.get_primals() + self.assertIn(m.x, sol) + self.assertIn(m.y, sol) + self.assertIn(m.z, sol) + + del m.c3 + del m.c4 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 0) + sol = res.solution_loader.get_primals() + self.assertIn(m.x, sol) + self.assertIn(m.y, sol) + self.assertNotIn(m.z, sol) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bug_1(self, name: str, opt_class: Type[SolverBase],): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(3, 7)) + m.y = pe.Var(bounds=(-10, 10)) + m.p = pe.Param(mutable=True, initialize=0) + + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y >= m.p * m.x) + + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 0) + + m.p.value = 1 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 3) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bug_2(self, name: str, opt_class: Type[SolverBase],): + """ + This test is for a bug where an objective containing a fixed variable does + not get updated properly when the variable is unfixed. + """ + for fixed_var_option in [True, False]: + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var() + m.obj = pe.Objective(expr=3 * m.y - m.x) + m.c = pe.Constraint(expr=m.y >= m.x) + + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2, 5) + + m.x.unfix() + m.x.setlb(-9) + m.x.setub(9) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -18, 5) + + +class TestLegacySolverInterface(unittest.TestCase): + @parameterized.expand(input=all_solvers) + def test_param_updates(self, name: str, opt_class: Type[SolverBase]): + opt = pe.SolverFactory(name + '_v2') + if not opt.available(exception_flag=False): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res = opt.solve(m) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=all_solvers) + def test_load_solutions(self, name: str, opt_class: Type[SolverBase]): + opt = pe.SolverFactory(name + '_v2') + if not opt.available(exception_flag=False): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.obj = pe.Objective(expr=m.x) + m.c = pe.Constraint(expr=(-1, m.x, 1)) + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + res = opt.solve(m, load_solutions=False) + pe.assert_optimal_termination(res) + self.assertIsNone(m.x.value) + self.assertNotIn(m.c, m.dual) + m.solutions.load_from(res) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.dual[m.c], 1) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index 807d66f569e..c4d13ae31d2 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -166,7 +166,7 @@ class DirectSolverUtils: class PersistentSolverUtils(abc.ABC): - def __init__(self, only_child_vars=False): + def __init__(self): self._model = None self._active_constraints = {} # maps constraint to (lower, body, upper) self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) @@ -185,11 +185,10 @@ def __init__(self, only_child_vars=False): self._vars_referenced_by_con = {} self._vars_referenced_by_obj = [] self._expr_types = None - self._only_child_vars = only_child_vars def set_instance(self, model): saved_config = self.config - self.__init__(only_child_vars=self._only_child_vars) + self.__init__() self.config = saved_config self._model = model self.add_block(model) @@ -257,8 +256,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): self._active_constraints[con] = (con.lower, con.body, con.upper) tmp = collect_vars_and_named_exprs(con.body) named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) + self._check_for_new_vars(variables) self._named_expressions[con] = [(e, e.expr) for e in named_exprs] if len(external_functions) > 0: self._external_functions[con] = external_functions @@ -285,8 +283,7 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): ) self._active_constraints[con] = tuple() variables = con.get_variables() - if not self._only_child_vars: - self._check_for_new_vars(variables) + self._check_for_new_vars(variables) self._named_expressions[con] = [] self._vars_referenced_by_con[con] = variables for v in variables: @@ -301,8 +298,7 @@ def set_objective(self, obj: _GeneralObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_obj) + self._check_to_remove_vars(self._vars_referenced_by_obj) self._external_functions.pop(self._objective, None) if obj is not None: self._objective = obj @@ -310,8 +306,7 @@ def set_objective(self, obj: _GeneralObjectiveData): self._objective_sense = obj.sense tmp = collect_vars_and_named_exprs(obj.expr) named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) + self._check_for_new_vars(variables) self._obj_named_expressions = [(i, i.expr) for i in named_exprs] if len(external_functions) > 0: self._external_functions[obj] = external_functions @@ -339,15 +334,6 @@ def add_block(self, block): for _p in p.values(): param_dict[id(_p)] = _p self.add_params(list(param_dict.values())) - if self._only_child_vars: - self.add_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects(Var, descend_into=True) - ).values() - ) - ) self.add_constraints( list( block.component_data_objects(Constraint, descend_into=True, active=True) @@ -379,8 +365,7 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): ) for v in self._vars_referenced_by_con[con]: self._referenced_variables[id(v)][0].pop(con) - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_con[con]) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) del self._active_constraints[con] del self._named_expressions[con] self._external_functions.pop(con, None) @@ -454,17 +439,6 @@ def remove_block(self, block): ) ) ) - if self._only_child_vars: - self.remove_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects( - ctype=Var, descend_into=True - ) - ).values() - ) - ) self.remove_params( list( dict( @@ -512,20 +486,7 @@ def update(self, timer: HierarchicalTimer = None): current_cons_dict = {} current_sos_dict = {} timer.start('vars') - if self._only_child_vars and ( - config.check_for_new_or_removed_vars or config.update_vars - ): - current_vars_dict = { - id(v): v - for v in self._model.component_data_objects(Var, descend_into=True) - } - for v_id, v in current_vars_dict.items(): - if v_id not in self._vars: - new_vars.append(v) - for v_id, v_tuple in self._vars.items(): - if v_id not in current_vars_dict: - old_vars.append(v_tuple[0]) - elif config.update_vars: + if config.update_vars: start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} timer.stop('vars') timer.start('params') @@ -636,12 +597,7 @@ def update(self, timer: HierarchicalTimer = None): self.add_sos_constraints(sos_to_update) timer.stop('cons') timer.start('vars') - if self._only_child_vars and config.update_vars: - vars_to_check = [] - for v_id, v in current_vars_dict.items(): - if v_id not in new_vars_set: - vars_to_check.append(v) - elif config.update_vars: + if config.update_vars: end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] if config.update_vars: From 4471b7caab4b120c4a00c627bc015fb9995962b3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 10 Feb 2024 21:33:18 -0700 Subject: [PATCH 0308/1178] run black --- pyomo/contrib/solver/gurobi.py | 25 ++- pyomo/contrib/solver/ipopt.py | 16 +- pyomo/contrib/solver/sol_reader.py | 4 +- .../solver/tests/solvers/test_solvers.py | 144 ++++++------------ 4 files changed, 76 insertions(+), 113 deletions(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 2dcdacd320d..50d241e1e88 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -69,12 +69,12 @@ def __init__( visibility=visibility, ) self.use_mipstart: bool = self.declare( - 'use_mipstart', + 'use_mipstart', ConfigValue( - default=False, - domain=bool, + default=False, + domain=bool, description="If True, the values of the integer variables will be passed to Gurobi.", - ) + ), ) @@ -339,9 +339,12 @@ def _solve(self): self._solver_model.setParam('MIPGap', config.rel_gap) if config.abs_gap is not None: self._solver_model.setParam('MIPGapAbs', config.abs_gap) - + if config.use_mipstart: - for pyomo_var_id, gurobi_var in self._pyomo_var_to_solver_var_map.items(): + for ( + pyomo_var_id, + gurobi_var, + ) in self._pyomo_var_to_solver_var_map.items(): pyomo_var = self._vars[pyomo_var_id][0] if pyomo_var.is_integer() and pyomo_var.value is not None: self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) @@ -866,7 +869,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.provenInfeasible elif status == grb.INF_OR_UNBD: @@ -894,7 +899,11 @@ def _postsolve(self, timer: HierarchicalTimer): else: results.termination_condition = TerminationCondition.unknown - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied and config.raise_exception_on_nonoptimal_result: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + and config.raise_exception_on_nonoptimal_result + ): raise RuntimeError( 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' ) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 4c4b932381d..5eb877f0867 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -89,15 +89,15 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[float] = ( - self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) - ) + self.timing_info.no_function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) ) - self.timing_info.function_solve_time: Optional[float] = ( - self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) - ) + self.timing_info.function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) ) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index a2e4d90b898..04f12feb25e 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -122,7 +122,9 @@ def parse_sol_file( # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? result.solution_status = SolutionStatus.infeasible - result.termination_condition = TerminationCondition.iterationLimit # this is not always correct + result.termination_condition = ( + TerminationCondition.iterationLimit + ) # this is not always correct elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 0499f1abb5d..658aaf41b13 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -21,10 +21,7 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [ - ('gurobi', Gurobi), - ('ipopt', ipopt), -] +all_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] mip_solvers = [('gurobi', Gurobi)] nlp_solvers = [('ipopt', ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] @@ -43,7 +40,7 @@ def _load_tests(solver_list): class TestSolvers(unittest.TestCase): @parameterized.expand(input=_load_tests(all_solvers)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[SolverBase], + self, name: str, opt_class: Type[SolverBase] ): # this test is for issue #2888 opt: SolverBase = opt_class() @@ -65,9 +62,7 @@ def test_remove_variable_and_objective( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_stale_vars( - self, name: str, opt_class: Type[SolverBase], - ): + def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -108,9 +103,7 @@ def test_stale_vars( self.assertFalse(m.y.stale) @parameterized.expand(input=_load_tests(all_solvers)) - def test_range_constraint( - self, name: str, opt_class: Type[SolverBase], - ): + def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -131,9 +124,7 @@ def test_range_constraint( self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs( - self, name: str, opt_class: Type[SolverBase], - ): + def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -150,9 +141,7 @@ def test_reduced_costs( self.assertAlmostEqual(rc[m.y], 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs2( - self, name: str, opt_class: Type[SolverBase], - ): + def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -172,9 +161,7 @@ def test_reduced_costs2( self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_param_changes( - self, name: str, opt_class: Type[SolverBase], - ): + def test_param_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -210,9 +197,7 @@ def test_param_changes( self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_immutable_param( - self, name: str, opt_class: Type[SolverBase], - ): + def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. @@ -252,9 +237,7 @@ def test_immutable_param( self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_equality( - self, name: str, opt_class: Type[SolverBase], - ): + def test_equality(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -292,9 +275,7 @@ def test_equality( self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_linear_expression( - self, name: str, opt_class: Type[SolverBase], - ): + def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -332,9 +313,7 @@ def test_linear_expression( self.assertTrue(bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers)) - def test_no_objective( - self, name: str, opt_class: Type[SolverBase], - ): + def test_no_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -365,9 +344,7 @@ def test_no_objective( self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_remove_cons( - self, name: str, opt_class: Type[SolverBase], - ): + def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -421,9 +398,7 @@ def test_add_remove_cons( self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_results_infeasible( - self, name: str, opt_class: Type[SolverBase], - ): + def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -472,7 +447,7 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers)) - def test_duals(self, name: str, opt_class: Type[SolverBase],): + def test_duals(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -496,7 +471,7 @@ def test_duals(self, name: str, opt_class: Type[SolverBase],): @parameterized.expand(input=_load_tests(qcp_solvers)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[SolverBase], + self, name: str, opt_class: Type[SolverBase] ): opt: SolverBase = opt_class() if not opt.available(): @@ -519,9 +494,7 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) @parameterized.expand(input=_load_tests(qcp_solvers)) - def test_mutable_quadratic_objective( - self, name: str, opt_class: Type[SolverBase], - ): + def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -546,13 +519,13 @@ def test_mutable_quadratic_objective( self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): for treat_fixed_vars_as_params in [True, False]: opt: SolverBase = opt_class() if opt.is_persistent(): - opt.config.auto_updates.treat_fixed_vars_as_params = treat_fixed_vars_as_params + opt.config.auto_updates.treat_fixed_vars_as_params = ( + treat_fixed_vars_as_params + ) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -587,9 +560,7 @@ def test_fixed_vars( self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_2( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -627,9 +598,7 @@ def test_fixed_vars_2( self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_3( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -645,9 +614,7 @@ def test_fixed_vars_3( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_fixed_vars_4( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -667,9 +634,7 @@ def test_fixed_vars_4( self.assertAlmostEqual(m.y.value, 2**0.5) @parameterized.expand(input=_load_tests(all_solvers)) - def test_mutable_param_with_range( - self, name: str, opt_class: Type[SolverBase], - ): + def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -743,7 +708,10 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value + 1e-12) + self.assertTrue( + res.objective_bound is None + or res.objective_bound <= m.y.value + 1e-12 + ) duals = res.solution_loader.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -751,15 +719,16 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound is None or res.objective_bound >= m.y.value - 1e-12) + self.assertTrue( + res.objective_bound is None + or res.objective_bound >= m.y.value - 1e-12 + ) duals = res.solution_loader.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_and_remove_vars( - self, name: str, opt_class: Type[SolverBase], - ): + def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): raise unittest.SkipTest @@ -805,7 +774,7 @@ def test_add_and_remove_vars( self.assertAlmostEqual(m.y.value, -1) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_exp(self, name: str, opt_class: Type[SolverBase],): + def test_exp(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): raise unittest.SkipTest @@ -819,7 +788,7 @@ def test_exp(self, name: str, opt_class: Type[SolverBase],): self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_log(self, name: str, opt_class: Type[SolverBase],): + def test_log(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): raise unittest.SkipTest @@ -833,9 +802,7 @@ def test_log(self, name: str, opt_class: Type[SolverBase],): self.assertAlmostEqual(m.y.value, -0.42630274815985264) @parameterized.expand(input=_load_tests(all_solvers)) - def test_with_numpy( - self, name: str, opt_class: Type[SolverBase], - ): + def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -863,9 +830,7 @@ def test_with_numpy( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bounds_with_params( - self, name: str, opt_class: Type[SolverBase], - ): + def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -897,9 +862,7 @@ def test_bounds_with_params( self.assertAlmostEqual(m.y.value, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_solution_loader( - self, name: str, opt_class: Type[SolverBase], - ): + def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -949,9 +912,7 @@ def test_solution_loader( self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_time_limit( - self, name: str, opt_class: Type[SolverBase], - ): + def test_time_limit(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1002,13 +963,12 @@ def test_time_limit( opt.config.raise_exception_on_nonoptimal_result = False res = opt.solve(m) self.assertIn( - res.termination_condition, {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit} + res.termination_condition, + {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit}, ) @parameterized.expand(input=_load_tests(all_solvers)) - def test_objective_changes( - self, name: str, opt_class: Type[SolverBase], - ): + def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1072,9 +1032,7 @@ def test_objective_changes( self.assertAlmostEqual(res.incumbent_objective, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_domain( - self, name: str, opt_class: Type[SolverBase], - ): + def test_domain(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1098,9 +1056,7 @@ def test_domain( self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_domain_with_integers( - self, name: str, opt_class: Type[SolverBase], - ): + def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1124,9 +1080,7 @@ def test_domain_with_integers( self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_binaries( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1153,9 +1107,7 @@ def test_fixed_binaries( self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_with_gdp( - self, name: str, opt_class: Type[SolverBase], - ): + def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1248,7 +1200,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_1(self, name: str, opt_class: Type[SolverBase],): + def test_bug_1(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1271,7 +1223,7 @@ def test_bug_1(self, name: str, opt_class: Type[SolverBase],): self.assertAlmostEqual(res.incumbent_objective, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_2(self, name: str, opt_class: Type[SolverBase],): + def test_bug_2(self, name: str, opt_class: Type[SolverBase]): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. From 90c98c85e254221b44b7b82d66f6ccb674958acb Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 12 Feb 2024 12:12:04 -0700 Subject: [PATCH 0309/1178] Reformatted parmest files using black. --- .../parameter_estimation_example.py | 4 +- pyomo/contrib/parmest/deprecated/parmest.py | 27 ++-- .../parmest/deprecated/tests/test_examples.py | 24 ++- .../parmest/deprecated/tests/test_parmest.py | 4 +- .../simple_reaction_parmest_example.py | 20 +-- .../reactor_design/bootstrap_example.py | 8 +- .../confidence_region_example.py | 8 +- .../reactor_design/datarec_example.py | 23 +-- .../reactor_design/leaveNout_example.py | 7 +- .../likelihood_ratio_example.py | 10 +- .../multisensor_data_example.py | 33 ++-- .../parameter_estimation_example.py | 10 +- .../examples/reactor_design/reactor_design.py | 54 ++++--- .../reactor_design/timeseries_data_example.py | 14 +- .../rooney_biegler/bootstrap_example.py | 10 +- .../likelihood_ratio_example.py | 10 +- .../parameter_estimation_example.py | 12 +- .../examples/rooney_biegler/rooney_biegler.py | 13 +- .../rooney_biegler_with_constraint.py | 13 +- .../semibatch/parameter_estimation_example.py | 9 +- .../examples/semibatch/scenario_example.py | 6 +- .../parmest/examples/semibatch/semibatch.py | 10 +- pyomo/contrib/parmest/experiment.py | 5 +- pyomo/contrib/parmest/parmest.py | 152 +++++++++--------- pyomo/contrib/parmest/scenariocreator.py | 20 +-- pyomo/contrib/parmest/tests/test_parmest.py | 115 +++++++------ .../parmest/tests/test_scenariocreator.py | 6 +- 27 files changed, 337 insertions(+), 290 deletions(-) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py index 67b69c73555..f5d9364097e 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py @@ -41,9 +41,9 @@ def SSE(model, data): # Parameter estimation obj, theta = pest.theta_est() - print (obj) + print(obj) print(theta) - + # Assert statements compare parameter estimation (theta) to an expected value k1_expected = 5.0 / 6.0 k2_expected = 5.0 / 3.0 diff --git a/pyomo/contrib/parmest/deprecated/parmest.py b/pyomo/contrib/parmest/deprecated/parmest.py index cbdc9179f35..82bf893dd06 100644 --- a/pyomo/contrib/parmest/deprecated/parmest.py +++ b/pyomo/contrib/parmest/deprecated/parmest.py @@ -548,14 +548,13 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) if self.diagnostic_mode: @@ -745,14 +744,10 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py index 04aff572529..6f5d9703f05 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_examples.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_examples.py @@ -32,7 +32,9 @@ def tearDownClass(self): pass def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import rooney_biegler + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( + rooney_biegler, + ) rooney_biegler.main() @@ -53,7 +55,9 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( + bootstrap_example, + ) bootstrap_example.main() @@ -136,7 +140,9 @@ def tearDownClass(self): @unittest.pytest.mark.expensive def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import reactor_design + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + reactor_design, + ) reactor_design.main() @@ -149,7 +155,9 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + bootstrap_example, + ) bootstrap_example.main() @@ -163,7 +171,9 @@ def test_likelihood_ratio_example(self): @unittest.pytest.mark.expensive def test_leaveNout_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import leaveNout_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + leaveNout_example, + ) leaveNout_example.main() @@ -183,7 +193,9 @@ def test_multisensor_data_example(self): @unittest.skipUnless(matplotlib_available, "test requires matplotlib") def test_datarec_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import datarec_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + datarec_example, + ) datarec_example.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py index 40c98dac3af..27776bdc64c 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py @@ -411,9 +411,7 @@ def rooney_biegler_indexed_vars(data): model.theta = pyo.Var( model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta[ - "asymptote" - ].fixed = ( + model.theta["asymptote"].fixed = ( True # parmest will unfix theta variables, even when they are indexed ) model.theta["rate_constant"].fixed = True diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 140fceeb8a2..4e9bc6079e7 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -98,32 +98,32 @@ def label_model(self): def get_labeled_model(self): self.create_model() m = self.label_model() - + return m + # k[2] fixed class SimpleReactionExperimentK2Fixed(SimpleReactionExperiment): def label_model(self): - + m = super().label_model() m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k[1]]) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.k[1]]) return m + # k[2] variable class SimpleReactionExperimentK2Variable(SimpleReactionExperiment): def label_model(self): - + m = super().label_model() m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k[1], m.k[2]]) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.k[1], m.k[2]]) return m @@ -150,7 +150,7 @@ def main(): ] # Create an experiment list with k[2] fixed - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SimpleReactionExperimentK2Fixed(data[i])) @@ -162,7 +162,7 @@ def main(): # Parameter estimation without covariance estimate # Only estimate the parameter k[1]. The parameter k[2] will remain fixed # at its initial value - + pest = parmest.Estimator(exp_list) obj, theta = pest.theta_est() print(obj) @@ -170,7 +170,7 @@ def main(): print() # Create an experiment list with k[2] variable - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SimpleReactionExperimentK2Variable(data[i])) diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index b5cb4196456..0935fbbba41 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -16,18 +16,19 @@ ReactorDesignExperiment, ) + def main(): # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) @@ -50,5 +51,6 @@ def main(): title="Bootstrap theta with confidence regions", ) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py index ff84279018d..8aee6e9d67c 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -16,18 +16,19 @@ ReactorDesignExperiment, ) + def main(): # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) @@ -45,5 +46,6 @@ def main(): CR = pest.confidence_region_test(bootstrap_theta, "MVN", [0.5, 0.75, 1.0]) print(CR) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 26185290ea6..2945c284d4a 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -29,11 +29,12 @@ def reactor_design_model_for_datarec(): return model + class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): - super().__init__(data, experiment_number) + super().__init__(data, experiment_number) self.data_std = data_std def create_model(self): @@ -43,7 +44,7 @@ def create_model(self): def label_model(self): m = self.model - + # experiment outputs m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) @@ -63,11 +64,12 @@ def label_model(self): return m + class ReactorDesignExperimentPostDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): - super().__init__(data, experiment_number) + super().__init__(data, experiment_number) self.data_std = data_std def label_model(self): @@ -83,6 +85,7 @@ def label_model(self): return m + def generate_data(): ### Generate data based on real sv, caf, ca, cb, cc, and cd @@ -117,14 +120,16 @@ def main(): data_std = data.std() # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) # Define sum of squared error objective function for data rec def SSE(model): - expr = sum(((y - yhat)/model.experiment_outputs_std[y])**2 - for y, yhat in model.experiment_outputs.items()) + expr = sum( + ((y - yhat) / model.experiment_outputs_std[y]) ** 2 + for y, yhat in model.experiment_outputs.items() + ) return expr # View one model & SSE @@ -138,7 +143,7 @@ def SSE(model): obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) print(theta) - + parmest.graphics.grouped_boxplot( data[["ca", "cb", "cc", "cd"]], data_rec[["ca", "cb", "cc", "cd"]], @@ -149,7 +154,7 @@ def SSE(model): data_rec["sv"] = data["sv"] # make a new list of experiments using reconciled data - exp_list= [] + exp_list = [] for i in range(data_rec.shape[0]): exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) @@ -157,7 +162,7 @@ def SSE(model): obj, theta = pest.theta_est() print(obj) print(theta) - + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} print(theta_real) diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index 549233d8a84..97aad04c325 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -24,7 +24,7 @@ def main(): file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create more data for the example N = 50 df_std = data.std().to_frame().transpose() @@ -33,10 +33,10 @@ def main(): data = df_sample + df_rand.dot(df_std) / 10 # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) @@ -89,5 +89,6 @@ def main(): percent_true = sum(r) / len(r) print(percent_true) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index 8b6d9fcfecc..7af37b64931 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -20,17 +20,17 @@ def main(): - -# Read in data + + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index f731032368e..a3802d40360 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -21,34 +21,37 @@ class MultisensorReactorDesignExperiment(ReactorDesignExperiment): def finalize_model(self): - + m = self.model - + # Experiment inputs values m.sv = self.data_i['sv'] m.caf = self.data_i['caf'] - + # Experiment output values - m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1/3) + m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1 / 3) m.cb = self.data_i['cb'] - m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1/2) + m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1 / 2) m.cd = self.data_i['cd'] - + return m def label_model(self): m = self.model - + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])]) + m.experiment_outputs.update( + [(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])] + ) m.experiment_outputs.update([(m.cb, [self.data_i['cb']])]) m.experiment_outputs.update([(m.cc, [self.data_i['cc1'], self.data_i['cc2']])]) m.experiment_outputs.update([(m.cd, [self.data_i['cd']])]) - + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k1, m.k2, m.k3]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] + ) return m @@ -60,9 +63,9 @@ def main(): file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(MultisensorReactorDesignExperiment(data, i)) @@ -72,9 +75,9 @@ def SSE_multisensor(model): for y, yhat in model.experiment_outputs.items(): num_outputs = len(yhat) for i in range(num_outputs): - expr += ((y - yhat[i])**2) * (1 / num_outputs) + expr += ((y - yhat[i]) ** 2) * (1 / num_outputs) return expr - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 76744984cce..4da7dd13023 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -23,19 +23,19 @@ def main(): file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) pest = parmest.Estimator(exp_list, obj_function='SSE') - + # Parameter estimation with covariance obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) print(obj) - print(theta) \ No newline at end of file + print(theta) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index db3b0e1d380..e524a6dd90e 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -21,19 +21,26 @@ from pyomo.contrib.parmest.experiment import Experiment + def reactor_design_model(): # Create the concrete model model = pyo.ConcreteModel() # Rate constants - model.k1 = pyo.Param(initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True) # min^-1 - model.k2 = pyo.Param(initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True) # min^-1 - model.k3 = pyo.Param(initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True) # m^3/(gmol min) + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) # Inlet concentration of A, gmol/m^3 model.caf = pyo.Param(initialize=10000, within=pyo.PositiveReals, mutable=True) - + # Space velocity (flowrate/volume) model.sv = pyo.Param(initialize=1.0, within=pyo.PositiveReals, mutable=True) @@ -61,63 +68,68 @@ def reactor_design_model(): expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) ) - model.cc_bal = pyo.Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) model.cd_bal = pyo.Constraint( expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) return model - + + class ReactorDesignExperiment(Experiment): - - def __init__(self, data, experiment_number): + + def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number - self.data_i = data.loc[experiment_number,:] + self.data_i = data.loc[experiment_number, :] self.model = None - + def create_model(self): self.model = m = reactor_design_model() return m - + def finalize_model(self): m = self.model - + # Experiment inputs values m.sv = self.data_i['sv'] m.caf = self.data_i['caf'] - + # Experiment output values m.ca = self.data_i['ca'] m.cb = self.data_i['cb'] m.cc = self.data_i['cc'] m.cd = self.data_i['cd'] - + return m def label_model(self): m = self.model - + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) - + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k1, m.k2, m.k3]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] + ) return m - + def get_labeled_model(self): m = self.create_model() m = self.finalize_model() m = self.label_model() - + return m + def main(): # For a range of sv values, return ca, cb, cc, and cd @@ -143,6 +155,6 @@ def main(): results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) + if __name__ == "__main__": main() - \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index a9a5ab20b54..59d6a26ca23 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -20,25 +20,25 @@ class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): - def __init__(self, data, experiment_number): + def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number self.data_i = data[experiment_number] self.model = None - + def finalize_model(self): m = self.model - + # Experiment inputs values m.sv = self.data_i['sv'] m.caf = self.data_i['caf'] - + # Experiment output values m.ca = self.data_i['ca'][0] m.cb = self.data_i['cb'][0] m.cc = self.data_i['cc'][0] m.cd = self.data_i['cd'][0] - + return m @@ -92,7 +92,7 @@ def main(): data_ts = group_data(data, 'experiment', ['sv', 'caf']) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data_ts)): exp_list.append(TimeSeriesReactorDesignExperiment(data_ts, i)) @@ -102,7 +102,7 @@ def SSE_timeseries(model): for y, yhat in model.experiment_outputs.items(): num_time_points = len(yhat) for i in range(num_time_points): - expr += ((y - yhat[i])**2) * (1 / num_time_points) + expr += ((y - yhat[i]) ** 2) * (1 / num_time_points) return expr diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 1f15ab95779..b79bc8e4c5a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -26,14 +26,16 @@ def main(): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index 869bb39efb9..a8daac79e23 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -28,14 +28,16 @@ def main(): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index b6ca7af0ab6..1f73f1cfdb0 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -26,14 +26,16 @@ def main(): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) # View one model # exp0_model = exp_list[0].get_labeled_model() @@ -41,7 +43,7 @@ def SSE(model): # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) - + # Parameter estimation and covariance n = 6 # total number of data points used in the objective (y in 6 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 6e7d6219a64..2b75c8621a7 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -28,11 +28,11 @@ def rooney_biegler_model(data): model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr - + model.response_function = pyo.Expression(data.hour, rule=response_rule) def SSE_rule(m): @@ -63,13 +63,14 @@ def label_model(self): m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.asymptote, m.rate_constant]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] + ) def finalize_model(self): m = self.model - + # Experiment output values m.hour = self.data.iloc[0]['hour'] m.y = self.data.iloc[0]['y'] @@ -78,7 +79,7 @@ def get_labeled_model(self): self.create_model() self.label_model() self.finalize_model() - + return self.model diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 1e213684a01..11100a8a40f 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -28,7 +28,7 @@ def rooney_biegler_model_with_constraint(data): model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - + model.response_function = pyo.Var(data.hour, initialize=0.0) # changed from expression to constraint @@ -48,6 +48,7 @@ def SSE_rule(m): return model + class RooneyBieglerExperiment(Experiment): def __init__(self, data): @@ -65,15 +66,15 @@ def label_model(self): m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) - m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.asymptote, m.rate_constant]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] + ) def finalize_model(self): m = self.model - + # Experiment output values m.hour = self.data.iloc[0]['hour'] m.y = self.data.iloc[0]['y'] @@ -82,7 +83,7 @@ def get_labeled_model(self): self.create_model() self.label_model() self.finalize_model() - + return self.model diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index 145569f7535..d74f094cd4f 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -12,9 +12,8 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import ( - SemiBatchExperiment, -) +from pyomo.contrib.parmest.examples.semibatch.semibatch import SemiBatchExperiment + def main(): @@ -28,7 +27,7 @@ def main(): data.append(d) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SemiBatchExperiment(data[i])) @@ -40,7 +39,7 @@ def main(): # for sum of squared error that will be used in parameter estimation pest = parmest.Estimator(exp_list) - + obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index a80a82671bc..1270ef49839 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -12,9 +12,7 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import ( - SemiBatchExperiment, -) +from pyomo.contrib.parmest.examples.semibatch.semibatch import SemiBatchExperiment import pyomo.contrib.parmest.scenariocreator as sc @@ -30,7 +28,7 @@ def main(): data.append(d) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SemiBatchExperiment(data[i])) diff --git a/pyomo/contrib/parmest/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/examples/semibatch/semibatch.py index 3ef7bc01aa9..b882df7a015 100644 --- a/pyomo/contrib/parmest/examples/semibatch/semibatch.py +++ b/pyomo/contrib/parmest/examples/semibatch/semibatch.py @@ -283,11 +283,11 @@ def create_model(self): def label_model(self): m = self.model - - m.unknown_parameters = Suffix(direction=Suffix.LOCAL) - m.unknown_parameters.update((k, ComponentUID(k)) - for k in [m.k1, m.k2, m.E1, m.E2]) + m.unknown_parameters = Suffix(direction=Suffix.LOCAL) + m.unknown_parameters.update( + (k, ComponentUID(k)) for k in [m.k1, m.k2, m.E1, m.E2] + ) def finalize_model(self): pass @@ -296,7 +296,7 @@ def get_labeled_model(self): self.create_model() self.label_model() self.finalize_model() - + return self.model diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py index 73b18bb5975..e16ad304e42 100644 --- a/pyomo/contrib/parmest/experiment.py +++ b/pyomo/contrib/parmest/experiment.py @@ -1,15 +1,16 @@ # The experiment class is a template for making experiment lists # to pass to parmest. An experiment is a pyomo model "m" which has # additional suffixes: -# m.experiment_outputs -- which variables are experiment outputs +# m.experiment_outputs -- which variables are experiment outputs # m.unknown_parameters -- which variables are parameters to estimate # The experiment class has only one required method: # get_labeled_model() # which returns the labeled pyomo model. + class Experiment: def __init__(self, model=None): self.model = model def get_labeled_model(self): - return self.model \ No newline at end of file + return self.model diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index e256b0f38d7..7e9e6cf90d4 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -309,10 +309,12 @@ def _experiment_instance_creation_callback( # return grouped_data + def SSE(model): - expr = sum((y - yhat)**2 for y, yhat in model.experiment_outputs.items()) + expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr + class _SecondStageCostExpr(object): """ Class to pass objective expression into the Pyomo model @@ -332,10 +334,10 @@ class Estimator(object): Parameters ---------- experiement_list: list of Experiments - A list of experiment objects which creates one labeled model for + A list of experiment objects which creates one labeled model for each expeirment obj_function: string or function (optional) - Built in objective (currently only "SSE") or custom function used to + Built in objective (currently only "SSE") or custom function used to formulate parameter estimation objective. If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and @@ -351,50 +353,54 @@ class Estimator(object): # backwards compatible constructor will accept the old inputs # from parmest_deprecated as well as the new inputs using experiment lists def __init__(self, *args, **kwargs): - + # check that we have at least one argument - assert(len(args) > 0) + assert len(args) > 0 # use deprecated interface self.pest_deprecated = None if callable(args[0]): - logger.warning('Using deprecated parmest inputs (model_function, ' + - 'data, theta_names), please use experiment lists instead.') + logger.warning( + 'Using deprecated parmest inputs (model_function, ' + + 'data, theta_names), please use experiment lists instead.' + ) self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) return # check that we have a (non-empty) list of experiments - assert (isinstance(args[0], list)) - assert (len(args[0]) > 0) + assert isinstance(args[0], list) + assert len(args[0]) > 0 self.exp_list = args[0] # check that an experiment has experiment_outputs and unknown_parameters model = self.exp_list[0].get_labeled_model() try: - outputs = [k.name for k,v in model.experiment_outputs.items()] + outputs = [k.name for k, v in model.experiment_outputs.items()] except: - RuntimeError('Experiment list model does not have suffix ' + - '"experiment_outputs".') + RuntimeError( + 'Experiment list model does not have suffix ' + '"experiment_outputs".' + ) try: - parms = [k.name for k,v in model.unknown_parameters.items()] + parms = [k.name for k, v in model.unknown_parameters.items()] except: - RuntimeError('Experiment list model does not have suffix ' + - '"unknown_parameters".') - + RuntimeError( + 'Experiment list model does not have suffix ' + '"unknown_parameters".' + ) + # populate keyword argument options self.obj_function = kwargs.get('obj_function', None) self.tee = kwargs.get('tee', False) self.diagnostic_mode = kwargs.get('diagnostic_mode', False) self.solver_options = kwargs.get('solver_options', None) - # TODO This might not be needed here. + # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built theta_names = [] for experiment in self.exp_list: model = experiment.get_labeled_model() - theta_names.extend([k.name for k,v in model.unknown_parameters.items()]) + theta_names.extend([k.name for k, v in model.unknown_parameters.items()]) self.estimator_theta_names = list(set(theta_names)) - + self._second_stage_cost_exp = "SecondStageCost" # boolean to indicate if model is initialized using a square solve self.model_initialized = False @@ -406,7 +412,7 @@ def _return_theta_names(self): # check for deprecated inputs if self.pest_deprecated is not None: - # if fitted model parameter names differ from theta_names + # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.pest_deprecated.theta_names_updated @@ -418,7 +424,7 @@ def _return_theta_names(self): else: - # if fitted model parameter names differ from theta_names + # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.theta_names_updated @@ -434,7 +440,7 @@ def _create_parmest_model(self, experiment_number): """ model = self.exp_list[experiment_number].get_labeled_model() - self.theta_names = [k.name for k,v in model.unknown_parameters.items()] + self.theta_names = [k.name for k, v in model.unknown_parameters.items()] if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) @@ -458,7 +464,7 @@ def _create_parmest_model(self, experiment_number): # TODO, this needs to be turned a enum class of options that still support custom functions if self.obj_function == 'SSE': - second_stage_rule=_SecondStageCostExpr(SSE) + second_stage_rule = _SecondStageCostExpr(SSE) else: # A custom function uses model.experiment_outputs as data second_stage_rule = _SecondStageCostExpr(self.obj_function) @@ -466,7 +472,6 @@ def _create_parmest_model(self, experiment_number): model.FirstStageCost = pyo.Expression(expr=0) model.SecondStageCost = pyo.Expression(rule=second_stage_rule) - def TotalCost_rule(model): return model.FirstStageCost + model.SecondStageCost @@ -534,7 +539,7 @@ def _Q_opt( outer_cb_data["ThetaVals"] = ThetaVals if bootlist is not None: outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = None # None is OK + outer_cb_data["cb_data"] = None # None is OK outer_cb_data["theta_names"] = self.estimator_theta_names options = {"solver": "ipopt"} @@ -581,14 +586,13 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) if self.diagnostic_mode: @@ -710,13 +714,13 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): "callback": self._instance_creation_callback, "ThetaVals": thetavals, "theta_names": self._return_theta_names(), - "cb_data": None, + "cb_data": None, } else: dummy_cb = { "callback": self._instance_creation_callback, "theta_names": self._return_theta_names(), - "cb_data": None, + "cb_data": None, } if self.diagnostic_mode: @@ -779,14 +783,10 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", @@ -944,21 +944,28 @@ def theta_est( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.theta_est( - solver=solver, + solver=solver, return_values=return_values, calc_cov=calc_cov, - cov_n=cov_n) - + cov_n=cov_n, + ) + assert isinstance(solver, str) assert isinstance(return_values, list) assert isinstance(calc_cov, bool) if calc_cov: - num_unknowns = max([len(experiment.get_labeled_model().unknown_parameters) - for experiment in self.exp_list]) - assert isinstance(cov_n, int), \ - "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" - assert cov_n > num_unknowns, \ - "The number of datapoints must be greater than the number of parameters to estimate" + num_unknowns = max( + [ + len(experiment.get_labeled_model().unknown_parameters) + for experiment in self.exp_list + ] + ) + assert isinstance( + cov_n, int + ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert ( + cov_n > num_unknowns + ), "The number of datapoints must be greater than the number of parameters to estimate" return self._Q_opt( solver=solver, @@ -1007,7 +1014,8 @@ def theta_est_bootstrap( samplesize=samplesize, replacement=replacement, seed=seed, - return_samples=return_samples) + return_samples=return_samples, + ) assert isinstance(bootstrap_samples, int) assert isinstance(samplesize, (type(None), int)) @@ -1068,10 +1076,8 @@ def theta_est_leaveNout( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.theta_est_leaveNout( - lNo, - lNo_samples=lNo_samples, - seed=seed, - return_samples=return_samples) + lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples + ) assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) @@ -1150,11 +1156,8 @@ def leaveNout_bootstrap_test( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.leaveNout_bootstrap_test( - lNo, - lNo_samples, - bootstrap_samples, - distribution, alphas, - seed=seed) + lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=seed + ) assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) @@ -1189,8 +1192,8 @@ def leaveNout_bootstrap_test( # expand indexed variables to get full list of thetas def _expand_indexed_unknowns(self, model_temp): - model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] - + model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] + # check for indexed theta items indexed_theta_list = [] for theta_i in model_theta_list: @@ -1198,7 +1201,7 @@ def _expand_indexed_unknowns(self, model_temp): var_validate = var_cuid.find_component_on(model_temp) for ind in var_validate.index_set(): if ind is not None: - indexed_theta_list.append(theta_i + '[' + str(ind) + ']') + indexed_theta_list.append(theta_i + '[' + str(ind) + ']') else: indexed_theta_list.append(theta_i) @@ -1233,15 +1236,16 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if self.pest_deprecated is not None: return self.pest_deprecated.objective_at_theta( theta_values=theta_values, - initialize_parmest_model=initialize_parmest_model) + initialize_parmest_model=initialize_parmest_model, + ) - if len(self.estimator_theta_names) == 0: + if len(self.estimator_theta_names) == 0: pass # skip assertion if model has no fitted parameters else: # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) - + # # iterate over original theta_names # for theta_i in self.theta_names: # var_cuid = ComponentUID(theta_i) @@ -1354,10 +1358,8 @@ def likelihood_ratio_test( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.likelihood_ratio_test( - obj_at_theta, - obj_value, - alphas, - return_thresholds=return_thresholds) + obj_at_theta, obj_value, alphas, return_thresholds=return_thresholds + ) assert isinstance(obj_at_theta, pd.DataFrame) assert isinstance(obj_value, (int, float)) @@ -1415,10 +1417,8 @@ def confidence_region_test( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.confidence_region_test( - theta_values, - distribution, - alphas, - test_theta_values=test_theta_values) + theta_values, distribution, alphas, test_theta_values=test_theta_values + ) assert isinstance(theta_values, pd.DataFrame) assert distribution in ['Rect', 'MVN', 'KDE'] diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index c48ac2bf027..82d13ce2007 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -17,8 +17,10 @@ import pyomo.contrib.parmest.deprecated.scenariocreator as scen_deprecated import logging + logger = logging.getLogger(__name__) + class ScenarioSet(object): """ Class to hold scenario sets @@ -127,10 +129,13 @@ def __init__(self, pest, solvername): # is this a deprecated pest object? self.scen_deprecated = None if pest.pest_deprecated is not None: - logger.warning("Using a deprecated parmest object for scenario " + - "creator, please recreate object using experiment lists.") + logger.warning( + "Using a deprecated parmest object for scenario " + + "creator, please recreate object using experiment lists." + ) self.scen_deprecated = scen_deprecated.ScenarioCreator( - pest.pest_deprecated, solvername) + pest.pest_deprecated, solvername + ) else: self.pest = pest self.solvername = solvername @@ -148,7 +153,7 @@ def ScenariosFromExperiments(self, addtoSet): if self.scen_deprecated is not None: self.scen_deprecated.ScenariosFromExperiments(addtoSet) return - + assert isinstance(addtoSet, ScenarioSet) scenario_numbers = list(range(len(self.pest.exp_list))) @@ -156,9 +161,7 @@ def ScenariosFromExperiments(self, addtoSet): prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, - ) + model = self.pest._instance_creation_callback(exp_num) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) @@ -180,8 +183,7 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): # check if using deprecated pest object if self.scen_deprecated is not None: - self.scen_deprecated.ScenariosFromBootstrap( - addtoSet, numtomake, seed=seed) + self.scen_deprecated.ScenariosFromBootstrap(addtoSet, numtomake, seed=seed) return assert isinstance(addtoSet, ScenarioSet) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index b88871e0dbc..ff8d1663bc9 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -47,6 +47,7 @@ testdir = os.path.dirname(os.path.abspath(__file__)) + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -66,14 +67,18 @@ def setUp(self): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append( + RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) + ) # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) @@ -82,10 +87,7 @@ def SSE(model): self.data = data self.pest = parmest.Estimator( - exp_list, - obj_function=SSE, - solver_options=solver_options, - tee=True, + exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) def test_theta_est(self): @@ -372,7 +374,7 @@ def rooney_biegler_params(data): model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -389,7 +391,9 @@ def create_model(self): rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_params_exp_list.append( - RooneyBieglerExperimentParams(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentParams( + self.data.loc[i, :].to_frame().transpose() + ) ) def rooney_biegler_indexed_params(data): @@ -429,13 +433,14 @@ def label_model(self): m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.theta]) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) rooney_biegler_indexed_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( - RooneyBieglerExperimentIndexedParams(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentIndexedParams( + self.data.loc[i, :].to_frame().transpose() + ) ) def rooney_biegler_vars(data): @@ -465,7 +470,7 @@ def create_model(self): rooney_biegler_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_vars_exp_list.append( - RooneyBieglerExperimentVars(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentVars(self.data.loc[i, :].to_frame().transpose()) ) def rooney_biegler_indexed_vars(data): @@ -475,9 +480,7 @@ def rooney_biegler_indexed_vars(data): model.theta = pyo.Var( model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta[ - "asymptote" - ].fixed = ( + model.theta["asymptote"].fixed = ( True # parmest will unfix theta variables, even when they are indexed ) model.theta["rate_constant"].fixed = True @@ -509,20 +512,22 @@ def label_model(self): m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.theta]) - + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) rooney_biegler_indexed_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( - RooneyBieglerExperimentIndexedVars(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentIndexedVars( + self.data.loc[i, :].to_frame().transpose() + ) ) # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr self.objective_function = SSE @@ -570,13 +575,14 @@ def SSE(model): not parmest.inverse_reduced_hessian_available, "Cannot test covariance matrix: required ASL dependency is missing", ) - def check_rooney_biegler_results(self, objval, cov): # get indices in covariance matrix cov_cols = cov.columns.to_list() asymptote_index = [idx for idx, s in enumerate(cov_cols) if 'asymptote' in s][0] - rate_constant_index = [idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s][0] + rate_constant_index = [ + idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s + ][0] self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( @@ -596,8 +602,7 @@ def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -608,10 +613,9 @@ def test_parmest_basics(self): def test_parmest_basics_with_initialize_parmest_model_option(self): - for model_type, parmest_input in self.input.items(): + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -627,8 +631,7 @@ def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) obj_at_theta = pest.objective_at_theta( @@ -643,10 +646,9 @@ def test_parmest_basics_with_square_problem_solve(self): def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): - + pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) @@ -654,6 +656,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) self.check_rooney_biegler_results(objval, cov) + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -692,13 +695,15 @@ def setUp(self): ) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) solver_options = {"max_iter": 6000} - self.pest = parmest.Estimator(exp_list, obj_function='SSE', solver_options=solver_options) + self.pest = parmest.Estimator( + exp_list, obj_function='SSE', solver_options=solver_options + ) def test_theta_est(self): # used in data reconciliation @@ -820,15 +825,16 @@ def create_model(self): def label_model(self): m = self.model - + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k1, m.k2]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2] + ) def get_labeled_model(self): self.create_model() self.label_model() - + return self.model # This example tests data formatted in 3 ways @@ -873,10 +879,14 @@ def get_labeled_model(self): self.pest_dict = parmest.Estimator(exp_list_dict) # Estimator object with multiple scenarios - exp_list_df_multiple = [ReactorDesignExperimentDAE(data_df), - ReactorDesignExperimentDAE(data_df)] - exp_list_dict_multiple = [ReactorDesignExperimentDAE(data_dict), - ReactorDesignExperimentDAE(data_dict)] + exp_list_df_multiple = [ + ReactorDesignExperimentDAE(data_df), + ReactorDesignExperimentDAE(data_df), + ] + exp_list_dict_multiple = [ + ReactorDesignExperimentDAE(data_dict), + ReactorDesignExperimentDAE(data_dict), + ] self.pest_df_multiple = parmest.Estimator(exp_list_df_multiple) self.pest_dict_multiple = parmest.Estimator(exp_list_dict_multiple) @@ -963,27 +973,26 @@ def setUp(self): data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], ) - + # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr exp_list = [] for i in range(data.shape[0]): exp_list.append( - RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) ) solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( - exp_list, - obj_function=SSE, - solver_options=solver_options, - tee=True, + exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) def test_theta_est_with_square_initialization(self): diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index bf6fa12b8b1..0c0976a453f 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -65,9 +65,9 @@ def setUp(self): ], columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) @@ -123,7 +123,7 @@ def setUp(self): # for the sum of squared error that will be used in parameter estimation # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(sb.SemiBatchExperiment(data[i])) From 70d66e6d2fba6d6d2ca3b9ab5f347ca38c3176ef Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 12 Feb 2024 12:27:22 -0700 Subject: [PATCH 0310/1178] Fixed typo parmest.py. --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 7e9e6cf90d4..ccdec527c06 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -333,7 +333,7 @@ class Estimator(object): Parameters ---------- - experiement_list: list of Experiments + experiment_list: list of Experiments A list of experiment objects which creates one labeled model for each expeirment obj_function: string or function (optional) From 922c715013e62553f5551b338f6d6cd490358bb3 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 12 Feb 2024 12:32:19 -0700 Subject: [PATCH 0311/1178] Another typo in parmest.py. --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ccdec527c06..2e44b278423 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -381,7 +381,7 @@ def __init__(self, *args, **kwargs): 'Experiment list model does not have suffix ' + '"experiment_outputs".' ) try: - parms = [k.name for k, v in model.unknown_parameters.items()] + params = [k.name for k, v in model.unknown_parameters.items()] except: RuntimeError( 'Experiment list model does not have suffix ' + '"unknown_parameters".' From dac2f31c38d8be12f629f4fb5e322d564579696b Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 13 Feb 2024 09:32:54 -0500 Subject: [PATCH 0312/1178] fix lbb solve_data bug --- pyomo/contrib/gdpopt/branch_and_bound.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/branch_and_bound.py b/pyomo/contrib/gdpopt/branch_and_bound.py index 26dc2b5f2eb..645e3564a88 100644 --- a/pyomo/contrib/gdpopt/branch_and_bound.py +++ b/pyomo/contrib/gdpopt/branch_and_bound.py @@ -230,12 +230,12 @@ def _solve_gdp(self, model, config): no_feasible_soln = float('inf') self.LB = ( node_data.obj_lb - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -no_feasible_soln ) self.UB = ( no_feasible_soln - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -node_data.obj_lb ) config.logger.info( From 2a3b5731b6b73f8c1ba3b7831c1cdfc9c0a988f2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 12:34:58 -0700 Subject: [PATCH 0313/1178] Unit tests for config, results, and util complete --- pyomo/contrib/solver/config.py | 6 +- pyomo/contrib/solver/results.py | 103 ++++++++++++------ pyomo/contrib/solver/sol_reader.py | 8 +- pyomo/contrib/solver/solution.py | 6 +- .../contrib/solver/tests/unit/test_config.py | 61 ++++++++++- .../contrib/solver/tests/unit/test_results.py | 6 +- .../solver/tests/unit/test_solution.py | 18 ++- pyomo/contrib/solver/tests/unit/test_util.py | 32 +++++- 8 files changed, 187 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index d5921c526b0..2a1a129d1ac 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -63,7 +63,8 @@ def __init__( ConfigValue( domain=str, default=None, - description="The directory in which generated files should be saved. This replaced the `keepfiles` option.", + description="The directory in which generated files should be saved. " + "This replaced the `keepfiles` option.", ), ) self.load_solutions: bool = self.declare( @@ -79,7 +80,8 @@ def __init__( ConfigValue( domain=bool, default=True, - description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + description="If False, the `solve` method will continue processing " + "even if the returned result is nonoptimal.", ), ) self.symbolic_solver_labels: bool = self.declare( diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 1fa9d653d01..5ed6de44430 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -22,18 +22,12 @@ NonNegativeFloat, ADVANCED_OPTION, ) -from pyomo.common.errors import PyomoException from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) - - -class SolverResultsError(PyomoException): - """ - General exception to catch solver system errors - """ +from pyomo.common.timing import HierarchicalTimer class TerminationCondition(enum.Enum): @@ -167,12 +161,16 @@ class Results(ConfigDict): iteration_count: int The total number of iterations. timing_info: ConfigDict - A ConfigDict containing three pieces of information: - start_time: UTC timestamp of when run was initiated + A ConfigDict containing two pieces of information: + start_timestamp: UTC timestamp of when run was initiated wall_time: elapsed wall clock time for entire process - solver_wall_time: elapsed wall clock time for solve call + timer: a HierarchicalTimer object containing timing data about the solve extra_info: ConfigDict A ConfigDict to store extra information such as solver messages. + solver_configuration: ConfigDict + A copy of the SolverConfig ConfigDict, for later inspection/reproducibility. + solver_log: str + (ADVANCED OPTION) Any solver log messages. """ def __init__( @@ -191,41 +189,85 @@ def __init__( visibility=visibility, ) - self.solution_loader = self.declare('solution_loader', ConfigValue()) + self.solution_loader = self.declare( + 'solution_loader', + ConfigValue( + description="Object for loading the solution back into the model." + ), + ) self.termination_condition: TerminationCondition = self.declare( 'termination_condition', ConfigValue( - domain=In(TerminationCondition), default=TerminationCondition.unknown + domain=In(TerminationCondition), + default=TerminationCondition.unknown, + description="The reason the solver exited. This is a member of the " + "TerminationCondition enum.", ), ) self.solution_status: SolutionStatus = self.declare( 'solution_status', - ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), + ConfigValue( + domain=In(SolutionStatus), + default=SolutionStatus.noSolution, + description="The result of the solve call. This is a member of " + "the SolutionStatus enum.", + ), ) self.incumbent_objective: Optional[float] = self.declare( - 'incumbent_objective', ConfigValue(domain=float, default=None) + 'incumbent_objective', + ConfigValue( + domain=float, + default=None, + description="If a feasible solution was found, this is the objective " + "value of the best solution found. If no feasible solution was found, this is None.", + ), ) self.objective_bound: Optional[float] = self.declare( - 'objective_bound', ConfigValue(domain=float, default=None) + 'objective_bound', + ConfigValue( + domain=float, + default=None, + description="The best objective bound found. For minimization problems, " + "this is the lower bound. For maximization problems, this is the " + "upper bound. For solvers that do not provide an objective bound, " + "this should be -inf (minimization) or inf (maximization)", + ), ) self.solver_name: Optional[str] = self.declare( - 'solver_name', ConfigValue(domain=str) + 'solver_name', + ConfigValue(domain=str, description="The name of the solver in use."), ) self.solver_version: Optional[Tuple[int, ...]] = self.declare( - 'solver_version', ConfigValue(domain=tuple) + 'solver_version', + ConfigValue( + domain=tuple, + description="A tuple representing the version of the solver in use.", + ), ) self.iteration_count: Optional[int] = self.declare( - 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) + 'iteration_count', + ConfigValue( + domain=NonNegativeInt, + default=None, + description="The total number of iterations.", + ), ) self.timing_info: ConfigDict = self.declare( 'timing_info', ConfigDict(implicit=True) ) self.timing_info.start_timestamp: datetime = self.timing_info.declare( - 'start_timestamp', ConfigValue(domain=Datetime) + 'start_timestamp', + ConfigValue( + domain=Datetime, description="UTC timestamp of when run was initiated." + ), ) self.timing_info.wall_time: Optional[float] = self.timing_info.declare( - 'wall_time', ConfigValue(domain=NonNegativeFloat) + 'wall_time', + ConfigValue( + domain=NonNegativeFloat, + description="Elapsed wall clock time for entire process.", + ), ) self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) @@ -233,13 +275,18 @@ def __init__( self.solver_configuration: ConfigDict = self.declare( 'solver_configuration', ConfigValue( - description="A copy of the config object used in the solve", + description="A copy of the config object used in the solve call.", visibility=ADVANCED_OPTION, ), ) self.solver_log: str = self.declare( 'solver_log', - ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), + ConfigValue( + domain=str, + default=None, + visibility=ADVANCED_OPTION, + description="Any solver log messages.", + ), ) def display( @@ -248,18 +295,6 @@ def display( return super().display(content_filter, indent_spacing, ostream, visibility) -class ResultsReader: - pass - - -def parse_yaml(): - pass - - -def parse_json(): - pass - - # Everything below here preserves backwards compatibility legacy_termination_condition_map = { diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 68654a4e9d7..3af30e1826b 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -15,7 +15,7 @@ from pyomo.common.errors import DeveloperError from pyomo.repn.plugins.nl_writer import NLWriterInfo -from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition +from .results import Results, SolutionStatus, TerminationCondition class SolFileData: @@ -69,7 +69,7 @@ def parse_sol_file( line = sol_file.readline() model_objects.append(int(line)) else: - raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") + raise Exception("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] @@ -85,12 +85,12 @@ def parse_sol_file( if line and ('objno' in line): exit_code_line = line.split() if len(exit_code_line) != 3: - raise SolverResultsError( + raise Exception( f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise SolverResultsError( + raise Exception( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) result.extra_info.solver_message = message.strip().replace('\n', '; ') diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 33a3b1c939c..beb53cf979a 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -23,6 +23,11 @@ class SolutionLoaderBase(abc.ABC): + """ + Base class for all future SolutionLoader classes. + + Intent of this class and its children is to load the solution back into the model. + """ def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: @@ -58,7 +63,6 @@ def get_primals( primals: ComponentMap Maps variables to solution values """ - pass def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index 4a7cc250623..f28dd5fcedf 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -10,7 +10,12 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.contrib.solver.config import SolverConfig, BranchAndBoundConfig +from pyomo.contrib.solver.config import ( + SolverConfig, + BranchAndBoundConfig, + AutoUpdateConfig, + PersistentSolverConfig, +) class TestSolverConfig(unittest.TestCase): @@ -59,3 +64,57 @@ def test_interface_custom_instantiation(self): self.assertIsInstance(config.time_limit, float) config.rel_gap = 2.5 self.assertEqual(config.rel_gap, 2.5) + + +class TestAutoUpdateConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = AutoUpdateConfig() + self.assertTrue(config.check_for_new_or_removed_constraints) + self.assertTrue(config.check_for_new_or_removed_vars) + self.assertTrue(config.check_for_new_or_removed_params) + self.assertTrue(config.check_for_new_objective) + self.assertTrue(config.update_constraints) + self.assertTrue(config.update_vars) + self.assertTrue(config.update_named_expressions) + self.assertTrue(config.update_objective) + self.assertTrue(config.update_objective) + self.assertTrue(config.treat_fixed_vars_as_params) + + def test_interface_custom_instantiation(self): + config = AutoUpdateConfig(description="A description") + config.check_for_new_objective = False + self.assertEqual(config._description, "A description") + self.assertTrue(config.check_for_new_or_removed_constraints) + self.assertFalse(config.check_for_new_objective) + + +class TestPersistentSolverConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = PersistentSolverConfig() + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + self.assertTrue(config.auto_updates.check_for_new_or_removed_constraints) + self.assertTrue(config.auto_updates.check_for_new_or_removed_vars) + self.assertTrue(config.auto_updates.check_for_new_or_removed_params) + self.assertTrue(config.auto_updates.check_for_new_objective) + self.assertTrue(config.auto_updates.update_constraints) + self.assertTrue(config.auto_updates.update_vars) + self.assertTrue(config.auto_updates.update_named_expressions) + self.assertTrue(config.auto_updates.update_objective) + self.assertTrue(config.auto_updates.update_objective) + self.assertTrue(config.auto_updates.treat_fixed_vars_as_params) + + def test_interface_custom_instantiation(self): + config = PersistentSolverConfig(description="A description") + config.tee = True + config.auto_updates.check_for_new_objective = False + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.auto_updates.check_for_new_objective) diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 23c2c32f819..caef82129ec 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -69,7 +69,7 @@ def test_codes(self): class TestResults(unittest.TestCase): - def test_declared_items(self): + def test_member_list(self): res = results.Results() expected_declared = { 'extra_info', @@ -88,7 +88,7 @@ def test_declared_items(self): actual_declared = res._declared self.assertEqual(expected_declared, actual_declared) - def test_uninitialized(self): + def test_default_initialization(self): res = results.Results() self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) @@ -118,7 +118,7 @@ def test_uninitialized(self): ): res.solution_loader.get_reduced_costs() - def test_results(self): + def test_generated_results(self): m = pyo.ConcreteModel() m.x = ScalarVar() m.y = ScalarVar() diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 1ecba45b32a..67ce2556317 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -10,22 +10,30 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.contrib.solver import solution +from pyomo.contrib.solver.solution import SolutionLoaderBase, PersistentSolutionLoader -class TestPersistentSolverBase(unittest.TestCase): +class TestSolutionLoaderBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = ['get_primals'] - member_list = list(solution.SolutionLoaderBase.__abstractmethods__) + member_list = list(SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) @unittest.mock.patch.multiple( - solution.SolutionLoaderBase, __abstractmethods__=set() + SolutionLoaderBase, __abstractmethods__=set() ) def test_solution_loader_base(self): - self.instance = solution.SolutionLoaderBase() + self.instance = SolutionLoaderBase() self.assertEqual(self.instance.get_primals(), None) with self.assertRaises(NotImplementedError): self.instance.get_duals() with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() + + +class TestPersistentSolutionLoader(unittest.TestCase): + def test_abstract_member_list(self): + # We expect no abstract members at this point because it's a real-life + # instantiation of SolutionLoaderBase + member_list = list(PersistentSolutionLoader('ipopt').__abstractmethods__) + self.assertEqual(member_list, []) diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py index 8a8a0221362..ab8a778067f 100644 --- a/pyomo/contrib/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -102,15 +102,41 @@ def test_check_optimal_termination_condition_legacy_interface(self): results = SolverResults() results.solver.status = SolverStatus.ok results.solver.termination_condition = LegacyTerminationCondition.optimal + # Both items satisfied self.assertTrue(check_optimal_termination(results)) + # Termination condition not satisfied results.solver.termination_condition = LegacyTerminationCondition.unknown self.assertFalse(check_optimal_termination(results)) + # Both not satisfied results.solver.termination_condition = SolverStatus.aborted self.assertFalse(check_optimal_termination(results)) - # TODO: Left off here; need to make these tests def test_assert_optimal_termination_new_interface(self): - pass + results = Results() + results.solution_status = SolutionStatus.optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + assert_optimal_termination(results) + # Termination condition not satisfied + results.termination_condition = TerminationCondition.iterationLimit + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + # Both not satisfied + results.solution_status = SolutionStatus.noSolution + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) def test_assert_optimal_termination_legacy_interface(self): - pass + results = SolverResults() + results.solver.status = SolverStatus.ok + results.solver.termination_condition = LegacyTerminationCondition.optimal + assert_optimal_termination(results) + # Termination condition not satisfied + results.solver.termination_condition = LegacyTerminationCondition.unknown + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + # Both not satisfied + results.solver.termination_condition = SolverStatus.aborted + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) From d4e56e81cb5d8ca354aad72b2da4cc2261f2ca05 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 12:38:41 -0700 Subject: [PATCH 0314/1178] Apply new version of black --- pyomo/contrib/solver/ipopt.py | 16 ++++++++-------- pyomo/contrib/solver/sol_reader.py | 4 +--- pyomo/contrib/solver/solution.py | 1 + pyomo/contrib/solver/tests/unit/test_solution.py | 4 +--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5eb877f0867..4c4b932381d 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -89,15 +89,15 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.no_function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) - self.timing_info.function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index f78d9fb3115..b9b33272fd6 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -90,9 +90,7 @@ def parse_sol_file( ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise Exception( - f"ERROR READING `sol` FILE. Expected `objno`; received {line}." - ) + raise Exception(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") result.extra_info.solver_message = message.strip().replace('\n', '; ') exit_code_message = '' if (exit_code[1] >= 0) and (exit_code[1] <= 99): diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index beb53cf979a..ca19e4df0e9 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -28,6 +28,7 @@ class SolutionLoaderBase(abc.ABC): Intent of this class and its children is to load the solution back into the model. """ + def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 67ce2556317..877be34d29b 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -19,9 +19,7 @@ def test_abstract_member_list(self): member_list = list(SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) - @unittest.mock.patch.multiple( - SolutionLoaderBase, __abstractmethods__=set() - ) + @unittest.mock.patch.multiple(SolutionLoaderBase, __abstractmethods__=set()) def test_solution_loader_base(self): self.instance = SolutionLoaderBase() self.assertEqual(self.instance.get_primals(), None) From 51dccea8e8835b5150abc68ef29429413a733554 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 13:28:36 -0700 Subject: [PATCH 0315/1178] Add unit tests for solution module --- pyomo/contrib/solver/sol_reader.py | 53 +++++++++---------- .../solver/tests/unit/test_solution.py | 45 ++++++++++++++++ 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index b9b33272fd6..ed4fe4865c2 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -13,7 +13,7 @@ from typing import Tuple, Dict, Any, List import io -from pyomo.common.errors import DeveloperError +from pyomo.common.errors import DeveloperError, PyomoException from pyomo.repn.plugins.nl_writer import NLWriterInfo from .results import Results, SolutionStatus, TerminationCondition @@ -26,6 +26,7 @@ def __init__(self) -> None: self.con_suffixes: Dict[str, Dict[Any]] = dict() self.obj_suffixes: Dict[str, Dict[int, Any]] = dict() self.problem_suffixes: Dict[str, List[Any]] = dict() + self.other: List(str) = list() def parse_sol_file( @@ -69,7 +70,7 @@ def parse_sol_file( line = sol_file.readline() model_objects.append(int(line)) else: - raise Exception("ERROR READING `sol` FILE. No 'Options' line found.") + raise PyomoException("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] @@ -85,12 +86,14 @@ def parse_sol_file( if line and ('objno' in line): exit_code_line = line.split() if len(exit_code_line) != 3: - raise Exception( + raise PyomoException( f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise Exception(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") + raise PyomoException( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) result.extra_info.solver_message = message.strip().replace('\n', '; ') exit_code_message = '' if (exit_code[1] >= 0) and (exit_code[1] <= 99): @@ -103,8 +106,6 @@ def parse_sol_file( elif (exit_code[1] >= 200) and (exit_code[1] <= 299): exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" result.solution_status = SolutionStatus.infeasible - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? result.termination_condition = TerminationCondition.locallyInfeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): exit_code_message = ( @@ -117,8 +118,6 @@ def parse_sol_file( "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " "was stopped by a limit that you set!" ) - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? result.solution_status = SolutionStatus.infeasible result.termination_condition = ( TerminationCondition.iterationLimit @@ -158,47 +157,47 @@ def parse_sol_file( line = sol_file.readline() result.extra_info.solver_message += remaining break - unmasked_kind = int(line[1]) - kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob + read_data_type = int(line[1]) + data_type = read_data_type & 3 # 0-var, 1-con, 2-obj, 3-prob convert_function = int - if (unmasked_kind & 4) == 4: + if (read_data_type & 4) == 4: convert_function = float - nvalues = int(line[2]) - # namelen = int(line[3]) - # tablen = int(line[4]) - tabline = int(line[5]) + number_of_entries = int(line[2]) + # The third entry is name length, and it is length+1. This is unnecessary + # except for data validation. + # The fourth entry is table "length", e.g., memory size. + number_of_string_lines = int(line[5]) suffix_name = sol_file.readline().strip() - # ignore translation of the table number to string value for now, - # this information can be obtained from the solver documentation - for n in range(tabline): - sol_file.readline() - if kind == 0: # Var + # Add any of arbitrary string lines to the "other" list + for line in range(number_of_string_lines): + sol_data.other.append(sol_file.readline()) + if data_type == 0: # Var sol_data.var_suffixes[suffix_name] = dict() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() var_ndx = int(suf_line[0]) sol_data.var_suffixes[suffix_name][var_ndx] = convert_function( suf_line[1] ) - elif kind == 1: # Con + elif data_type == 1: # Con sol_data.con_suffixes[suffix_name] = dict() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() con_ndx = int(suf_line[0]) sol_data.con_suffixes[suffix_name][con_ndx] = convert_function( suf_line[1] ) - elif kind == 2: # Obj + elif data_type == 2: # Obj sol_data.obj_suffixes[suffix_name] = dict() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() obj_ndx = int(suf_line[0]) sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function( suf_line[1] ) - elif kind == 3: # Prob + elif data_type == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() sol_data.problem_suffixes[suffix_name].append( convert_function(suf_line[1]) diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 877be34d29b..bbcc85bdac8 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -19,6 +19,15 @@ def test_abstract_member_list(self): member_list = list(SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) + def test_member_list(self): + expected_list = ['load_vars', 'get_primals', 'get_duals', 'get_reduced_costs'] + method_list = [ + method + for method in dir(SolutionLoaderBase) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(SolutionLoaderBase, __abstractmethods__=set()) def test_solution_loader_base(self): self.instance = SolutionLoaderBase() @@ -29,9 +38,45 @@ def test_solution_loader_base(self): self.instance.get_reduced_costs() +class TestSolSolutionLoader(unittest.TestCase): + # I am currently unsure how to test this further because it relies heavily on + # SolFileData and NLWriterInfo + def test_member_list(self): + expected_list = ['load_vars', 'get_primals', 'get_duals', 'get_reduced_costs'] + method_list = [ + method + for method in dir(SolutionLoaderBase) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + class TestPersistentSolutionLoader(unittest.TestCase): def test_abstract_member_list(self): # We expect no abstract members at this point because it's a real-life # instantiation of SolutionLoaderBase member_list = list(PersistentSolutionLoader('ipopt').__abstractmethods__) self.assertEqual(member_list, []) + + def test_member_list(self): + expected_list = [ + 'load_vars', + 'get_primals', + 'get_duals', + 'get_reduced_costs', + 'invalidate', + ] + method_list = [ + method + for method in dir(PersistentSolutionLoader) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_default_initialization(self): + # Realistically, a solver object should be passed into this. + # However, it works with a string. It'll just error loudly if you + # try to run get_primals, etc. + self.instance = PersistentSolutionLoader('ipopt') + self.assertTrue(self.instance._valid) + self.assertEqual(self.instance._solver, 'ipopt') From 571cc0860be6460363d92f08bca690fbbee4989d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 13:56:41 -0700 Subject: [PATCH 0316/1178] Add 'name' to SolverBase --- pyomo/contrib/solver/base.py | 7 +++++++ pyomo/contrib/solver/tests/unit/test_base.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 42524296d74..046a83fb7ec 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -54,6 +54,13 @@ class SolverBase(abc.ABC): CONFIG = SolverConfig() def __init__(self, **kwds) -> None: + # We allow the user and/or developer to name the solver something else, + # if they really desire. Otherwise it defaults to the class name (all lowercase) + if "name" in kwds: + self.name = kwds["name"] + kwds.pop('name') + else: + self.name = type(self).__name__.lower() self.config = self.CONFIG(value=kwds) # diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 00e38d9ac59..cda1631d921 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -41,6 +41,7 @@ def test_init(self): self.instance = base.SolverBase() self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.name, 'solverbase') self.assertEqual(self.instance.CONFIG, self.instance.config) self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) @@ -50,6 +51,7 @@ def test_context_manager(self): with base.SolverBase() as self.instance: self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.name, 'solverbase') self.assertEqual(self.instance.CONFIG, self.instance.config) self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) @@ -69,6 +71,11 @@ def test_solver_availability(self): self.instance.Availability.__bool__(self.instance.Availability) ) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_custom_solver_name(self): + self.instance = base.SolverBase(name='my_unique_name') + self.assertEqual(self.instance.name, 'my_unique_name') + class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): From b19c75aa02ba2249cc92f17612087a3df5c6e3dc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 14:08:21 -0700 Subject: [PATCH 0317/1178] Update documentation (which is still slim but is a reasonable start) --- .../developer_reference/solvers.rst | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 10e7e829463..fa24d69a211 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -9,8 +9,19 @@ Pyomo offers interfaces into multiple solvers, both commercial and open source. Interface Implementation ------------------------ -TBD: How to add a new interface; the pieces. +All new interfaces should be built upon one of two classes (currently): +``pyomo.contrib.solver.base.SolverBase`` or ``pyomo.contrib.solver.base.PersistentSolverBase``. +All solvers should have the following: + +.. autoclass:: pyomo.contrib.solver.base.SolverBase + :members: + +Persistent solvers should also include: + +.. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase + :show-inheritance: + :members: Results ------- @@ -56,4 +67,10 @@ returned solver messages or logs for more information. Solution -------- -TBD: How to load/parse a solution. +Solutions can be loaded back into a model using a ``SolutionLoader``. A specific +loader should be written for each unique case. Several have already been +implemented. For example, for ``ipopt``: + +.. autoclass:: pyomo.contrib.solver.solution.SolSolutionLoader + :show-inheritance: + :members: From 9ed93fe11fb2f8ee060614fb3b1fc639d746052b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 14:10:45 -0700 Subject: [PATCH 0318/1178] Update docs to point to new gurobi interface --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 046a83fb7ec..96b87924bf6 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -176,7 +176,7 @@ class PersistentSolverBase(SolverBase): methods from the direct solver base and adds those methods that are necessary for persistent solvers. - Example usage can be seen in solvers within APPSI. + Example usage can be seen in the GUROBI solver. """ def is_persistent(self): From 398493c32e2b193f76842a30fc58b52a352c7df7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 14 Feb 2024 14:24:21 -0700 Subject: [PATCH 0319/1178] solver refactor: update tests --- pyomo/contrib/solver/tests/solvers/test_solvers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 658aaf41b13..6b798f9bafd 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -139,6 +139,11 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): rc = res.solution_loader.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 3) self.assertAlmostEqual(rc[m.y], 4) + m.obj.expr *= -1 + res = opt.solve(m) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], -3) + self.assertAlmostEqual(rc[m.y], -4) @parameterized.expand(input=_load_tests(all_solvers)) def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): From 9fcb2366e82e515ea718057e4aa6645308943a88 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 15:23:02 -0700 Subject: [PATCH 0320/1178] Set up structure for sol_reader tests --- pyomo/contrib/solver/ipopt.py | 2 +- pyomo/contrib/solver/sol_reader.py | 2 +- pyomo/contrib/solver/solution.py | 2 +- .../solver/tests/unit/sol_files/bad_objno.sol | 22 + .../tests/unit/sol_files/bad_objnoline.sol | 22 + .../tests/unit/sol_files/bad_options.sol | 22 + .../tests/unit/sol_files/conopt_optimal.sol | 22 + .../tests/unit/sol_files/depr_solver.sol | 67 +++ .../unit/sol_files/iis_no_variable_values.sol | 34 ++ .../tests/unit/sol_files/infeasible1.sol | 491 ++++++++++++++++++ .../tests/unit/sol_files/infeasible2.sol | 13 + pyomo/contrib/solver/tests/unit/test_base.py | 14 +- .../solver/tests/unit/test_sol_reader.py | 51 ++ 13 files changed, 754 insertions(+), 10 deletions(-) create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol create mode 100644 pyomo/contrib/solver/tests/unit/test_sol_reader.py diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 4c4b932381d..f70cbb5f194 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -28,7 +28,7 @@ from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus -from .sol_reader import parse_sol_file +from pyomo.contrib.solver.sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolSolutionLoader, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index ed4fe4865c2..c4497516de2 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -15,7 +15,7 @@ from pyomo.common.errors import DeveloperError, PyomoException from pyomo.repn.plugins.nl_writer import NLWriterInfo -from .results import Results, SolutionStatus, TerminationCondition +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition class SolFileData: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index ca19e4df0e9..d4069b5b5a1 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -16,7 +16,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager -from .sol_reader import SolFileData +from pyomo.contrib.solver.sol_reader import SolFileData from pyomo.repn.plugins.nl_writer import NLWriterInfo from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol new file mode 100644 index 00000000000..a7eccfca388 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +Xobjno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol new file mode 100644 index 00000000000..6abcacbb3c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 1 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol new file mode 100644 index 00000000000..f59a2ffd3b4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +OXptions +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol b/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol new file mode 100644 index 00000000000..4ff14b50bc7 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol b/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol new file mode 100644 index 00000000000..01ceb566334 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol @@ -0,0 +1,67 @@ +PICO Solver: final f = 88.200000 + +Options +3 +0 +0 +0 +24 +24 +32 +32 +0 +0 +0.12599999999999997 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +46.666666666666664 +0 +0 +0 +0 +0 +0 +933.3333333333336 +10000 +10000 +10000 +10000 +0 +100 +0 +100 +0 +100 +0 +100 +46.666666666666664 +53.333333333333336 +0 +100 +0 +100 +0 +100 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol b/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol new file mode 100644 index 00000000000..641a3162a8f --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol @@ -0,0 +1,34 @@ +CPLEX 12.8.0.0: integer infeasible. +0 MIP simplex iterations +0 branch-and-bound nodes +Returning an IIS of 2 variables and 1 constraints. +No basis. + +Options +3 +1 +1 +0 +1 +0 +2 +0 +objno 0 220 +suffix 0 2 4 181 11 +iis + +0 non not in the iis +1 low at lower bound +2 fix fixed +3 upp at upper bound +4 mem member +5 pmem possible member +6 plow possibly at lower bound +7 pupp possibly at upper bound +8 bug + +0 1 +1 1 +suffix 1 1 4 0 0 +iis +0 4 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol b/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol new file mode 100644 index 00000000000..9e7c47f2091 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol @@ -0,0 +1,491 @@ + +Ipopt 3.12: Converged to a locally infeasible point. Problem may be infeasible. + +Options +3 +1 +1 +0 +242 +242 +86 +86 +-3.5031247438024307e-14 +-3.5234584915901186e-14 +-3.5172095867741636e-14 +-3.530546013164763e-14 +-3.5172095867741636e-14 +-3.5305460131648396e-14 +-2.366093398247632e-13 +-2.3660933995816667e-13 +-2.366093403160036e-13 +-2.366093402111279e-13 +-2.366093403160036e-13 +-2.366093402111279e-13 +-3.230618014133495e-14 +-3.229008861611988e-14 +-3.2372291959738883e-14 +-3.233107904711923e-14 +-3.2372291959738883e-14 +-3.233107904711986e-14 +-2.366093402825742e-13 +-2.3660934046399004e-13 +-2.366093408240676e-13 +-2.3660934074259244e-13 +-2.366093408240676e-13 +-2.3660934074259244e-13 +-3.5337260190603076e-15 +-3.5384985959538063e-15 +-3.5360752870197467e-15 +-3.5401103667524204e-15 +-3.5360752870197475e-15 +-3.540110366752954e-15 +-1.1241014244910024e-13 +-7.229408362081387e-14 +-1.1241014257725814e-13 +-7.229408365067014e-14 +-1.1241014257725814e-13 +-7.229408365067014e-14 +-0.045045044618550245 +-2.2503048100082865e-13 +-0.04504504461894986 +-2.3019280438209537e-13 +-2.4246742873024166e-13 +-2.3089017630512727e-13 +-2.303517676239642e-13 +-2.3258460904987257e-13 +-2.2657149778091163e-13 +-2.3561210481068387e-13 +-2.260257681221233e-13 +-2.4196851090379605e-13 +-2.2609595226592818e-13 +-0.04504504461900244 +-2.249595193064585e-13 +-0.04504504461913233 +-2.2215413967954347e-13 +-0.045045044619133334 +1.4720100770836167e-13 +0.5405405354313707 +-1.1746366725687393e-13 +-8.181817954545458e-14 +3.3628105937413004e-10 +2.5420446367682183e-10 +-4.068865957494519e-10 +-3.3083656247909664e-10 +2.0162505532975142e-10 +1.3899803000287233e-10 +1.9264257030343367e-10 +1.5784707460270425e-10 +4.0453655296452274e-10 +1.8623815108786813e-10 +4.023012427968502e-10 +2.2427204843237042e-10 +4.285852894154949e-10 +2.7438151967949997e-10 +4.990725722952413e-10 +3.24233733037425e-10 +6.365790489375267e-10 +1.8786461752037693e-10 +9.36934851555115e-10 +1.9328729420874646e-10 +2.1302900967163764e-09 +1.9184434624295806e-10 +1.839058810801874e-10 +3.1045038304739125e-08 +2.033627397720737e-10 +1.965179362792721e-09 +3.9014568630621037e-10 +9.629991995490913e-10 +3.8529492862465446e-10 +6.543016210883198e-10 +3.1023232285992586e-10 +5.203524431666233e-10 +2.443053484937026e-10 +4.814394103716646e-10 +1.9839047821553417e-10 +2.29157081595439e-10 +1.6697733108860693e-10 +2.2885043298472609e-10 +1.4439699240241691e-10 +2.231817349184844e-10 +7.996844380007978e-07 +7.95878555840714e-07 +-6.161782990947841e-09 +-6.174783045271923e-09 +-6.180473110458713e-09 +-6.1838001759594465e-09 +-6.180473110458713e-09 +-6.183800175957144e-09 +-1.3264604647361279e-14 +-1.3437580361963064e-14 +-1.381614108205247e-14 +-1.3724139850276759e-14 +-1.381614108205247e-14 +-1.3724139850276584e-14 +-1.3264604647361279e-14 +-1.3437580361963064e-14 +-1.381614108205247e-14 +-1.3724139850276759e-14 +-1.381614108205247e-14 +-1.3724139850276584e-14 +-1.3264604647357383e-14 +-1.3264604647357383e-14 +-1.258629585661237e-14 +-1.2586303131773045e-14 +-1.2586307639008801e-14 +-1.2586311120145482e-14 +-1.2586314285443517e-14 +-1.258631748040718e-14 +-1.2586321221671653e-14 +-1.2741959563395428e-14 +-1.2741955464025058e-14 +-1.2741952925774324e-14 +-1.2741950138083889e-14 +-1.2741945491635486e-14 +-1.274193825746462e-14 +-1.3437580361959015e-14 +-1.3437580361959015e-14 +-1.3437580361959015e-14 +-1.3816141082048241e-14 +-1.3816141082048241e-14 +-1.3081851406508949e-14 +-1.308185926540242e-14 +-1.3081864134282786e-14 +-1.3081867894733614e-14 +-1.308187131400409e-14 +-1.308187476532053e-14 +-1.3081878806771144e-14 +-1.2999353684840647e-14 +-1.299934941829921e-14 +-1.2999346776539415e-14 +-1.2999343875167873e-14 +-1.2999339039238868e-14 +-1.2999331510061096e-14 +-1.3724139850272537e-14 +-1.3724139850272537e-14 +-1.3724139850272537e-14 +-1.3816141082048243e-14 +-1.3816141082048243e-14 +-1.3081851406508949e-14 +-1.3081859265402422e-14 +-1.3081864134282784e-14 +-1.3081867894733614e-14 +-1.308187131400409e-14 +-1.308187476532053e-14 +-1.3081878806771145e-14 +-1.299935368484049e-14 +-1.2999349418299049e-14 +-1.2999346776539257e-14 +-1.2999343875167712e-14 +-1.299933903923871e-14 +-1.2999331510060935e-14 +-1.3724139850272359e-14 +-1.3724139850272359e-14 +-1.3724139850272359e-14 +-0.39647376852165084 +-0.4455844823264693 +-0.3964737698727394 +-0.4455844904349083 +-0.04058112126213324 +-2.37392784926522e-13 +-0.04058112126182639 +-2.3739125313713354e-13 +-2.3738581599973924e-13 +-2.3739030469186293e-13 +-2.373886019673396e-13 +-2.3738926304868226e-13 +-2.3739032800906814e-13 +-2.373875268840388e-13 +-2.3739166112281285e-13 +-2.373848238523691e-13 +-2.3739287329689576e-13 +-0.04058112126709927 +-2.3739409684312144e-13 +-0.04058112126734901 +-2.3739552961585984e-13 +-0.040581121263560345 +-7.976233462779415e-11 +-8.149038165921345e-11 +-8.149038165921345e-11 +-8.022671984428942e-11 +-8.112229180405433e-11 +-8.112229180405698e-11 +-1.1362727144888948e-10 +-4.545363318183219e-10 +-1.5766054471383136e-10 +-999.9999999987843 +2.0239864420785628e-10 +3.6952311802810024e-10 +2.123373938372435e-10 +2.804864327332228e-10 +1.346149969721881e-10 +2.2070281853153174e-10 +1.3486437441647496e-10 +1.837701666832909e-10 +1.3214731344936636e-10 +1.59848684557641e-10 +1.2663217798563007e-10 +1.4670685236091518e-10 +1.2005152713943525e-10 +2.1846147211317584e-10 +1.1320656639453056e-10 +2.1155957764572616e-10 +1.0602947953081767e-10 +2.1331568061293854e-10 +2.2406981587244565e-10 +1.0144323269437438e-10 +2.0067712609010725e-10 +1.0647572138657723e-10 +1.3628795523686926e-10 +1.1283736217061156e-10 +1.3689006597815967e-10 +1.1944117806753888e-10 +1.4976540231691364e-10 +1.2533138246033542e-10 +1.7219937613078787e-10 +1.2782000199367948e-10 +2.0576625901474408e-10 +1.8061506448741275e-10 +2.5564782647515365e-10 +1.8080595589290967e-10 +3.3611540082361537e-10 +1.8450853640157845e-10 +-999.9999999992634 +500.00000267889834 +3700.000036997707 +3700.00003699796 +3700.000036997707 +3700.00003699796 +3700.000036977598 +3700.000036977598 +11.65620349374497 +11.697892989049905 +11.723721175743378 +11.743669409189184 +11.761807757832353 +11.780116092441125 +11.801554922843986 +11.760485435103986 +11.737564481489017 +11.723372263570411 +11.70778533743834 +11.68180544764916 +11.64135667458445 +3700.000036977598 +3700.000036977598 +3700.000036977598 +0.3151184672323908 +0.32392866804605874 +0.34244076638380455 +0.33803566597697493 +0.34244076638380455 +0.3380356659769663 +0.27110063090377123 +0.2699297687440479 +0.2929786728909554 +0.29344480424126584 +0.28838393432428394 +0.2893992806145764 +0.2710728789062779 +0.26993404119945896 +0.2934152392453943 +0.29361001971947676 +0.2884212793214469 +0.28944447549328195 +0.2710728789062779 +0.2699340411994531 +0.29341523924539437 +0.29361001971947087 +0.28842127932144684 +0.2894444754932388 +0.5508615869879336 +0.15398873818985254 +0.6718832432569866 +0.17589826345513584 +0.5247189958883286 +0.18810973351399282 +0.6259675738420305 +0.20533542867213556 +0.7121098490801165 +0.23131269225729922 +0.7821527320463884 +0.28037348913556315 +0.8428067559035302 +0.5838840489481971 +0.8970272395501521 +0.6703093152878702 +0.94267886174376 +0.7738465562949745 +0.8177198430399907 +0.9786900926762641 +0.6704296542151029 +0.9210489338249574 +0.3564282839324347 +0.8691777702202935 +0.2593618184144545 +0.8137154539828636 +0.21644752420062746 +0.7494805564573437 +0.1955192721716388 +0.6636009115148781 +0.1816326651938952 +0.7714724374833359 +0.16783059150769936 +0.6720038647474075 +0.15295832306009652 +0.5820927246947017 +0 +5.999999940000606 +3.2342062150876796 +9.747775650827162 +objno 0 200 +suffix 4 60 13 0 0 +ipopt_zU_out +22 -1.327369555645263e-09 +23 -1.3446671271054377e-09 +24 -1.382523199114386e-09 +25 -1.373323075936809e-09 +26 -1.382523199114386e-09 +27 -1.3733230759367915e-09 +28 -1.2472104315043693e-09 +29 -1.2452101972496192e-09 +30 -1.2858040647227637e-09 +31 -1.2866523403876923e-09 +32 -1.2775019286011434e-09 +33 -1.2793272952136163e-09 +34 -1.2471629472231613e-09 +35 -1.2452174844060395e-09 +36 -1.2865985041388369e-09 +37 -1.2869532717202986e-09 +38 -1.2775689743171436e-09 +39 -1.2794086668147935e-09 +40 -1.2471629472231613e-09 +41 -1.2452174844060298e-09 +42 -1.2865985041388369e-09 +43 -1.2869532717202878e-09 +44 -1.2775689743171434e-09 +45 -1.2794086668147155e-09 +46 -2.0240773556752306e-09 +47 -1.0745612255836558e-09 +48 -2.770632290509263e-09 +49 -1.103129453565228e-09 +50 -1.9127440056903688e-09 +51 -1.1197213910483093e-09 +52 -2.430513566198766e-09 +53 -1.1439932412498466e-09 +54 -3.1577699873109563e-09 +55 -1.182653712929702e-09 +56 -4.173065268467735e-09 +57 -1.2632815552706913e-09 +58 -5.783269227344645e-09 +59 -2.1847056932251413e-09 +60 -8.828459262787896e-09 +61 -2.7574054223382863e-09 +62 -1.5860201572267072e-08 +63 -4.019796745114287e-09 +64 -4.987327799213503e-09 +65 -4.128677327837785e-08 +66 -2.7584122571707027e-09 +67 -1.1514963264478648e-08 +68 -1.4125712376227499e-09 +69 -6.9490543282105264e-09 +70 -1.2274426584743552e-09 +71 -4.880119585077116e-09 +72 -1.160216995366489e-09 +73 -3.628823630675873e-09 +74 -1.13003440308759e-09 +75 -2.7024178093492304e-09 +76 -1.1108592195439713e-09 +77 -3.978035995523888e-09 +78 -1.0924348929579286e-09 +79 -2.7716511991201962e-09 +80 -1.073254036073809e-09 +81 -2.175341139896496e-09 +suffix 4 86 13 0 0 +ipopt_zL_out +0 2.457002432427315e-13 +1 2.457002432427147e-13 +2 2.457002432427315e-13 +3 2.457002432427147e-13 +4 2.457002432440668e-13 +5 2.457002432440668e-13 +6 7.799202448711829e-11 +7 7.771407288173584e-11 +8 7.754286328443318e-11 +9 7.741114609420585e-11 +10 7.72917673061454e-11 +11 7.717164255304123e-11 +12 7.703145172513595e-11 +13 7.730045781990877e-11 +14 7.7451409084917e-11 +15 7.754517112285163e-11 +16 7.76484093372809e-11 +17 7.782109643810629e-11 +18 7.809149171545744e-11 +19 2.457002432440668e-13 +20 2.457002432440668e-13 +21 2.457002432440668e-13 +22 2.88491781594494e-09 +23 2.806453922602062e-09 +24 2.6547390725285084e-09 +25 2.6893342144319893e-09 +26 2.6547390725285084e-09 +27 2.6893342144320575e-09 +28 3.3533336782625715e-09 +29 3.367879281546927e-09 +30 3.1029251008167857e-09 +31 3.0979961649984553e-09 +32 3.152363115331538e-09 +33 3.1413031705213295e-09 +34 3.353676987058653e-09 +35 3.3678259755079893e-09 +36 3.0983083240635833e-09 +37 3.096252910785026e-09 +38 3.1519549450665203e-09 +39 3.1408126764021113e-09 +40 3.353676987058653e-09 +41 3.367825975508062e-09 +42 3.0983083240635824e-09 +43 3.0962529107850877e-09 +44 3.151954945066521e-09 +45 3.140812676402579e-09 +46 1.6503072927322882e-09 +47 5.903619062223097e-09 +48 1.3530489183372102e-09 +49 5.168276510428202e-09 +50 1.7325290303934247e-09 +51 4.8327689212818915e-09 +52 1.4522971044995076e-09 +53 4.4273454737645e-09 +54 1.276616097383978e-09 +55 3.930138360770138e-09 +56 1.1622933223262232e-09 +57 3.242428123819113e-09 +58 1.0786469044524248e-09 +59 1.556971619947646e-09 +60 1.0134484872637181e-09 +61 1.356225961423535e-09 +62 9.643698375125132e-10 +63 1.174768939146355e-09 +64 1.1117388275802617e-09 +65 9.288986889801197e-10 +66 1.3559825252250914e-09 +67 9.870172368223874e-10 +68 2.55055764727633e-09 +69 1.0459205566343963e-09 +70 3.5051068618760334e-09 +71 1.1172098225860037e-09 +72 4.2000521577056155e-09 +73 1.212961283078632e-09 +74 4.649622902405193e-09 +75 1.3699361786951016e-09 +76 5.005106744564875e-09 +77 1.1783841562800436e-09 +78 5.416717299785639e-09 +79 1.3528060526165563e-09 +80 5.943389257560972e-09 +81 1.561763024323873e-09 +82 500.00000026951534 +83 1.515151527777625e-10 +84 2.8108595681091103e-10 +85 9.326135918021712e-11 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol b/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol new file mode 100644 index 00000000000..6fddb053745 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol @@ -0,0 +1,13 @@ + + Couenne (C:\Users\SASCHA~1\AppData\Local\Temp\tmpvcmknhw0.pyomo.nl May 18 2015): Infeasible + +Options +3 +0 +1 +0 +242 +0 +86 +0 +objno 0 220 diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index cda1631d921..59a80ba270b 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -189,7 +189,7 @@ def test_class_method_list(self): def test_context_manager(self): with base.LegacySolverWrapper() as instance: - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): instance.available() def test_map_config(self): @@ -209,14 +209,14 @@ def test_map_config(self): self.assertFalse(instance.config.load_solutions) self.assertEqual(instance.config.time_limit, 20) # Report timing shouldn't be created because it no longer exists - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): print(instance.config.report_timing) # Keepfiles should not be created because we did not declare keepfiles on # the original config - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): print(instance.config.keepfiles) # We haven't implemented solver_io, suffixes, or logfile - with self.assertRaises(NotImplementedError) as context: + with self.assertRaises(NotImplementedError): instance._map_config( False, False, @@ -231,7 +231,7 @@ def test_map_config(self): None, None, ) - with self.assertRaises(NotImplementedError) as context: + with self.assertRaises(NotImplementedError): instance._map_config( False, False, @@ -246,7 +246,7 @@ def test_map_config(self): None, None, ) - with self.assertRaises(NotImplementedError) as context: + with self.assertRaises(NotImplementedError): instance._map_config( False, False, @@ -266,7 +266,7 @@ def test_map_config(self): False, False, False, 20, False, False, None, None, None, True, None, None ) self.assertEqual(instance.config.working_dir, os.getcwd()) - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): print(instance.config.keepfiles) def test_map_results(self): diff --git a/pyomo/contrib/solver/tests/unit/test_sol_reader.py b/pyomo/contrib/solver/tests/unit/test_sol_reader.py new file mode 100644 index 00000000000..0ab94dfc4ac --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_sol_reader.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.common.fileutils import this_file_dir +from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.solver.sol_reader import parse_sol_file, SolFileData + +currdir = this_file_dir() + + +class TestSolFileData(unittest.TestCase): + def test_default_instantiation(self): + instance = SolFileData() + self.assertIsInstance(instance.primals, list) + self.assertIsInstance(instance.duals, list) + self.assertIsInstance(instance.var_suffixes, dict) + self.assertIsInstance(instance.con_suffixes, dict) + self.assertIsInstance(instance.obj_suffixes, dict) + self.assertIsInstance(instance.problem_suffixes, dict) + self.assertIsInstance(instance.other, list) + + +class TestSolParser(unittest.TestCase): + # I am not sure how to write these tests best since the sol parser requires + # not only a file but also the nl_info and results objects. + def setUp(self): + TempfileManager.push() + + def tearDown(self): + TempfileManager.pop(remove=True) + + def test_default_behavior(self): + pass + + def test_custom_behavior(self): + pass + + def test_infeasible1(self): + pass + + def test_infeasible2(self): + pass From 273fd72d1587093b67eca832d9606f78b3a88b1b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 15:50:17 -0700 Subject: [PATCH 0321/1178] Add init file --- pyomo/contrib/solver/tests/unit/sol_files/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/__init__.py diff --git a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ From b0ecba2421219e46679c47578d82ce67bd15db04 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 14 Feb 2024 20:10:29 -0500 Subject: [PATCH 0322/1178] Update name and base class of solver arg exception --- pyomo/contrib/pyros/config.py | 8 ++++---- pyomo/contrib/pyros/tests/test_config.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 3256a333fdc..798e68b157f 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -15,7 +15,7 @@ InEnum, Path, ) -from pyomo.common.errors import ApplicationError +from pyomo.common.errors import ApplicationError, PyomoException from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory @@ -303,7 +303,7 @@ def domain_name(self): ) -class NotSolverResolvable(Exception): +class SolverNotResolvable(PyomoException): """ Exception type for failure to cast an object to a Pyomo solver. """ @@ -382,7 +382,7 @@ def __call__(self, obj, require_available=None, solver_desc=None): Raises ------ - NotSolverResolvable + SolverNotResolvable If `obj` cannot be cast to a Pyomo solver because it is neither a str nor a Pyomo solver type. ApplicationError @@ -405,7 +405,7 @@ def __call__(self, obj, require_available=None, solver_desc=None): elif self.is_solver_type(obj): solver = obj else: - raise NotSolverResolvable( + raise SolverNotResolvable( f"Cannot cast object `{obj!r}` to a Pyomo optimizer for use as " f"{solver_desc}, as the object is neither a str nor a " f"Pyomo Solver type (got type {type(obj).__name__})." diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 3113afaac89..eaed462a9b3 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -15,7 +15,7 @@ InputDataStandardizer, mutable_param_validator, LoggerType, - NotSolverResolvable, + SolverNotResolvable, PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, @@ -397,7 +397,7 @@ def test_solver_resolvable_invalid_type(self): r"Cannot cast object `2` to a Pyomo optimizer.*" r"local solver.*got type int.*" ) - with self.assertRaisesRegex(NotSolverResolvable, exc_str): + with self.assertRaisesRegex(SolverNotResolvable, exc_str): standardizer_func(invalid_object) def test_solver_resolvable_unavailable_solver(self): @@ -542,7 +542,7 @@ def test_solver_iterable_invalid_list(self): r"Cannot cast object `2` to a Pyomo optimizer.*" r"backup solver.*index 1.*got type int.*" ) - with self.assertRaisesRegex(NotSolverResolvable, exc_str): + with self.assertRaisesRegex(SolverNotResolvable, exc_str): standardizer_func(invalid_object) From f52db9d2efe9fdc1317183ad8c9e9f347d706bab Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 07:47:40 -0700 Subject: [PATCH 0323/1178] Update base member unit test - better checking logic --- pyomo/contrib/solver/tests/unit/test_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 59a80ba270b..b8d5c79fc0f 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -104,7 +104,6 @@ def test_class_method_list(self): expected_list = [ 'Availability', 'CONFIG', - '_abc_impl', '_get_duals', '_get_primals', '_get_reduced_costs', @@ -129,7 +128,7 @@ def test_class_method_list(self): method_list = [ method for method in dir(base.PersistentSolverBase) - if method.startswith('__') is False + if (method.startswith('__') or method.startswith('_abc')) is False ] self.assertEqual(sorted(expected_list), sorted(method_list)) From b99221cfaf58d1bada29f3e8d059c1ca4a6a1585 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 08:32:43 -0700 Subject: [PATCH 0324/1178] Address missing coverage for several modules --- pyomo/common/config.py | 2 ++ pyomo/common/tests/test_config.py | 18 +++++++++++++++ .../contrib/solver/tests/unit/test_results.py | 22 +++++++++++++++++++ .../solver/tests/unit/test_solution.py | 6 +++++ 4 files changed, 48 insertions(+) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 657765fdc02..4adb0299f0e 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -211,6 +211,8 @@ def Datetime(val): This domain will return the original object, assuming it is of the right type. """ + if val is None: + return val if not isinstance(val, datetime.datetime): raise ValueError(f"Expected datetime object, but received {type(val)}.") return val diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index e2a8c0fb591..ac23e4c54d3 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -25,6 +25,7 @@ # ___________________________________________________________________________ import argparse +import datetime import enum import os import os.path @@ -47,6 +48,7 @@ def yaml_load(arg): ConfigDict, ConfigValue, ConfigList, + Datetime, MarkImmutable, ImmutableConfigValue, Bool, @@ -738,6 +740,22 @@ def _rule(key, val): } ) + def test_Datetime(self): + c = ConfigDict() + c.declare('a', ConfigValue(domain=Datetime, default=None)) + self.assertEqual(c.get('a').domain_name(), 'Datetime') + + self.assertEqual(c.a, None) + c.a = datetime.datetime(2022, 1, 1) + self.assertEqual(c.a, datetime.datetime(2022, 1, 1)) + + with self.assertRaises(ValueError): + c.a = 5 + with self.assertRaises(ValueError): + c.a = 'Hello' + with self.assertRaises(ValueError): + c.a = False + class TestImmutableConfigValue(unittest.TestCase): def test_immutable_config_value(self): diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index caef82129ec..4672903cb43 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -9,6 +9,9 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import sys +from io import StringIO + from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.contrib.solver import results @@ -118,6 +121,25 @@ def test_default_initialization(self): ): res.solution_loader.get_reduced_costs() + def test_display(self): + res = results.Results() + stream = StringIO() + res.display(ostream=stream) + expected_print = """solution_loader: None +termination_condition: TerminationCondition.unknown +solution_status: SolutionStatus.noSolution +incumbent_objective: None +objective_bound: None +solver_name: None +solver_version: None +iteration_count: None +timing_info: + start_timestamp: None + wall_time: None +extra_info: +""" + self.assertEqual(expected_print, stream.getvalue()) + def test_generated_results(self): m = pyo.ConcreteModel() m.x = ScalarVar() diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index bbcc85bdac8..7a18344d4cb 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -80,3 +80,9 @@ def test_default_initialization(self): self.instance = PersistentSolutionLoader('ipopt') self.assertTrue(self.instance._valid) self.assertEqual(self.instance._solver, 'ipopt') + + def test_invalid(self): + self.instance = PersistentSolutionLoader('ipopt') + self.instance.invalidate() + with self.assertRaises(RuntimeError): + self.instance.get_primals() From a45e0854746ab4d54b69a8160bab8386ff3b07e0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 08:38:43 -0700 Subject: [PATCH 0325/1178] Add copyright statements; clean up unused imports --- pyomo/contrib/solver/gurobi.py | 26 +++++++++++++------ .../tests/solvers/test_gurobi_persistent.py | 15 ++++++++--- .../solver/tests/solvers/test_solvers.py | 12 ++++++++- .../contrib/solver/tests/unit/test_results.py | 1 - 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 50d241e1e88..919e7ae3995 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections.abc import Iterable import logging import math -from typing import List, Dict, Optional +from typing import List, Optional from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import @@ -9,10 +20,10 @@ from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer from pyomo.common.shutdown import python_is_shutting_down -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ConfigValue from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -28,7 +39,6 @@ import sys import datetime import io -from pyomo.contrib.solver.factory import SolverFactory logger = logging.getLogger(__name__) @@ -1137,9 +1147,9 @@ def set_linear_constraint_attr(self, con, attr, val): """ if attr in {'Sense', 'RHS', 'ConstrName'}: raise ValueError( - 'Linear constraint attr {0} cannot be set with' + 'Linear constraint attr {0} cannot be set with'.format(attr) + ' the set_linear_constraint_attr method. Please use' - + ' the remove_constraint and add_constraint methods.'.format(attr) + + ' the remove_constraint and add_constraint methods.' ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True @@ -1166,9 +1176,9 @@ def set_var_attr(self, var, attr, val): """ if attr in {'LB', 'UB', 'VType', 'VarName'}: raise ValueError( - 'Var attr {0} cannot be set with' + 'Var attr {0} cannot be set with'.format(attr) + ' the set_var_attr method. Please use' - + ' the update_var method.'.format(attr) + + ' the update_var method.' ) if attr == 'Obj': raise ValueError( diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py index f53088506f9..d4c0078a0df 100644 --- a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -1,9 +1,18 @@ -from pyomo.common.errors import PyomoException +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.contrib.solver.gurobi import Gurobi -from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus -from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.contrib.solver.results import SolutionStatus from pyomo.core.expr.taylor_series import taylor_series_expansion diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 6b798f9bafd..36f3596e890 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.common.dependencies import attempt_import import pyomo.common.unittest as unittest @@ -10,7 +21,6 @@ from pyomo.contrib.solver.gurobi import Gurobi from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression -import os import math numpy, numpy_available = attempt_import('numpy') diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 4672903cb43..8e16a1384ee 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import sys from io import StringIO from pyomo.common import unittest From 6d21b71c6c95b448ff4cb16ff4d8b646611becc7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 15 Feb 2024 08:52:17 -0700 Subject: [PATCH 0326/1178] updating docs --- doc/OnlineDocs/conf.py | 1 + .../developer_reference/solvers.rst | 35 +++++++++++-------- pyomo/contrib/solver/base.py | 29 +++++++-------- pyomo/contrib/solver/results.py | 4 +-- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index ef6510daedf..88f84ec8b37 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -72,6 +72,7 @@ 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx_copybutton', + 'enum_tools.autoenum', #'sphinx.ext.githubpages', ] diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index fa24d69a211..45d8e55daf9 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -10,7 +10,8 @@ Interface Implementation ------------------------ All new interfaces should be built upon one of two classes (currently): -``pyomo.contrib.solver.base.SolverBase`` or ``pyomo.contrib.solver.base.PersistentSolverBase``. +:class:`SolverBase` or +:class:`PersistentSolverBase`. All solvers should have the following: @@ -26,9 +27,11 @@ Persistent solvers should also include: Results ------- -Every solver, at the end of a ``solve`` call, will return a ``Results`` object. -This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar -to a standard ``dict`` in Python. +Every solver, at the end of a +:meth:`solve` call, will +return a :class:`Results` +object. This object is a :py:class:`pyomo.common.config.ConfigDict`, +which can be manipulated similar to a standard ``dict`` in Python. .. autoclass:: pyomo.contrib.solver.results.Results :show-inheritance: @@ -40,28 +43,29 @@ Termination Conditions ^^^^^^^^^^^^^^^^^^^^^^ Pyomo offers a standard set of termination conditions to map to solver -returns. The intent of ``TerminationCondition`` is to notify the user of why -the solver exited. The user is expected to inspect the ``Results`` object or any -returned solver messages or logs for more information. - - +returns. The intent of +:class:`TerminationCondition` +is to notify the user of why the solver exited. The user is expected +to inspect the :class:`Results` +object or any returned solver messages or logs for more information. .. autoclass:: pyomo.contrib.solver.results.TerminationCondition :show-inheritance: - :noindex: Solution Status ^^^^^^^^^^^^^^^ -Pyomo offers a standard set of solution statuses to map to solver output. The -intent of ``SolutionStatus`` is to notify the user of what the solver returned -at a high level. The user is expected to inspect the ``Results`` object or any +Pyomo offers a standard set of solution statuses to map to solver +output. The intent of +:class:`SolutionStatus` +is to notify the user of what the solver returned at a high level. The +user is expected to inspect the +:class:`Results` object or any returned solver messages or logs for more information. .. autoclass:: pyomo.contrib.solver.results.SolutionStatus :show-inheritance: - :noindex: Solution @@ -71,6 +75,7 @@ Solutions can be loaded back into a model using a ``SolutionLoader``. A specific loader should be written for each unique case. Several have already been implemented. For example, for ``ipopt``: -.. autoclass:: pyomo.contrib.solver.solution.SolSolutionLoader +.. autoclass:: pyomo.contrib.solver.ipopt.ipoptSolutionLoader :show-inheritance: :members: + :inherited-members: diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 96b87924bf6..aad8d10ec63 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -40,15 +40,18 @@ class SolverBase(abc.ABC): """ - Base class upon which direct solver interfaces can be built. - - This base class contains the required methods for all direct solvers: - - available: Determines whether the solver is able to be run, combining - both whether it can be found on the system and if the license is valid. - - config: The configuration method for solver objects. + This base class defines the methods required for all solvers: + - available: Determines whether the solver is able to be run, + combining both whether it can be found on the system and if the license is valid. - solve: The main method of every solver - version: The version of the solver - - is_persistent: Set to false for all direct solvers. + - is_persistent: Set to false for all non-persistent solvers. + + Additionally, solvers should have a :attr:`config` attribute that + inherits from one of :class:`SolverConfig`, + :class:`BranchAndBoundConfig`, + :class:`PersistentSolverConfig`, or + :class:`PersistentBranchAndBoundConfig`. """ CONFIG = SolverConfig() @@ -104,7 +107,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, **kwargs ) -> Results: """ Solve a Pyomo model. @@ -113,15 +116,13 @@ def solve( ---------- model: _BlockData The Pyomo model to be solved - timer: HierarchicalTimer - An option timer for reporting timing **kwargs Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) Returns ------- - results: Results + results: :class:`Results` A results object """ @@ -144,7 +145,7 @@ def available(self): Returns ------- - available: Solver.Availability + available: SolverBase.Availability An enum that indicates "how available" the solver is. Note that the enum can be cast to bool, which will be True if the solver is runable at all and False @@ -173,10 +174,10 @@ def is_persistent(self): class PersistentSolverBase(SolverBase): """ Base class upon which persistent solvers can be built. This inherits the - methods from the direct solver base and adds those methods that are necessary + methods from the solver base class and adds those methods that are necessary for persistent solvers. - Example usage can be seen in the GUROBI solver. + Example usage can be seen in the Gurobi interface. """ def is_persistent(self): diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 5ed6de44430..e80bad126a1 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -139,10 +139,10 @@ class Results(ConfigDict): ---------- solution_loader: SolutionLoaderBase Object for loading the solution back into the model. - termination_condition: TerminationCondition + termination_condition: :class:`TerminationCondition` The reason the solver exited. This is a member of the TerminationCondition enum. - solution_status: SolutionStatus + solution_status: :class:`SolutionStatus` The result of the solve call. This is a member of the SolutionStatus enum. incumbent_objective: float From 6c3739e6bd2c52a6ed3daaf3c14c92346ed31c5e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 09:53:56 -0700 Subject: [PATCH 0327/1178] Update docstrings; fix one test to account for pypy differences --- pyomo/contrib/solver/config.py | 33 ++++++++++++++----- .../contrib/solver/tests/unit/test_results.py | 5 ++- pyomo/contrib/solver/util.py | 2 +- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 2a1a129d1ac..e38f903e1ac 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -23,7 +23,7 @@ class SolverConfig(ConfigDict): """ - Base config values for all solver interfaces + Base config for all direct solver interfaces """ def __init__( @@ -118,13 +118,14 @@ def __init__( class BranchAndBoundConfig(SolverConfig): """ + Base config for all direct MIP solver interfaces + Attributes ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving + rel_gap: float + The relative value of the gap in relation to the best bound + abs_gap: float + The absolute value of the difference between the incumbent and best bound """ def __init__( @@ -144,10 +145,20 @@ def __init__( ) self.rel_gap: Optional[float] = self.declare( - 'rel_gap', ConfigValue(domain=NonNegativeFloat) + 'rel_gap', + ConfigValue( + domain=NonNegativeFloat, + description="Optional termination condition; the relative value of the " + "gap in relation to the best bound", + ), ) self.abs_gap: Optional[float] = self.declare( - 'abs_gap', ConfigValue(domain=NonNegativeFloat) + 'abs_gap', + ConfigValue( + domain=NonNegativeFloat, + description="Optional termination condition; the absolute value of the " + "difference between the incumbent and best bound", + ), ) @@ -315,6 +326,9 @@ def __init__( class PersistentSolverConfig(SolverConfig): + """ + Base config for all persistent solver interfaces + """ def __init__( self, description=None, @@ -337,6 +351,9 @@ def __init__( class PersistentBranchAndBoundConfig(BranchAndBoundConfig): + """ + Base config for all persistent MIP solver interfaces + """ def __init__( self, description=None, diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 8e16a1384ee..7b9de32bc00 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -137,7 +137,10 @@ def test_display(self): wall_time: None extra_info: """ - self.assertEqual(expected_print, stream.getvalue()) + out = stream.getvalue() + if 'null' in out: + out = out.replace('null', 'None') + self.assertEqual(expected_print, out) def test_generated_results(self): m = pyo.ConcreteModel() diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index c4d13ae31d2..af856eab7e2 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -16,7 +16,7 @@ import pyomo.core.expr as EXPR from pyomo.core.base.constraint import _GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.collections import ComponentMap From 220dd134dfdaf232561ea37efd45a64ef95e11fc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 10:02:37 -0700 Subject: [PATCH 0328/1178] Black and its empty lines --- pyomo/contrib/solver/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index e38f903e1ac..a1133f93ae4 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -329,6 +329,7 @@ class PersistentSolverConfig(SolverConfig): """ Base config for all persistent solver interfaces """ + def __init__( self, description=None, @@ -354,6 +355,7 @@ class PersistentBranchAndBoundConfig(BranchAndBoundConfig): """ Base config for all persistent MIP solver interfaces """ + def __init__( self, description=None, From 7edd9db575bf8e89e8b537fb34baf4e551276e09 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 12:48:51 -0700 Subject: [PATCH 0329/1178] Update documentation to include examples for usage --- .../developer_reference/solvers.rst | 74 ++++++++++++++++++- pyomo/contrib/solver/base.py | 8 -- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index fa24d69a211..ad0ade94f41 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -1,11 +1,81 @@ -Solver Interfaces -================= +Future Solver Interface Changes +=============================== Pyomo offers interfaces into multiple solvers, both commercial and open source. +To support better capabilities for solver interfaces, the Pyomo team is actively +redesigning the existing interfaces to make them more maintainable and intuitive +for use. Redesigned interfaces can be found in ``pyomo.contrib.solver``. .. currentmodule:: pyomo.contrib.solver +New Interface Usage +------------------- + +The new interfaces have two modes: backwards compatible and future capability. +To use the backwards compatible version, simply use the ``SolverFactory`` +as usual and replace the solver name with the new version. Currently, the new +versions available are: + +.. list-table:: Available Redesigned Solvers + :widths: 25 25 + :header-rows: 1 + + * - Solver + - ``SolverFactory`` Name + * - ipopt + - ``ipopt_v2`` + * - GUROBI + - ``gurobi_v2`` + +Backwards Compatible Mode +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt_v2').solve(model) + assert_optimal_termination(status) + model.pprint() + +Future Capability Mode +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.ipopt import ipopt + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = ipopt() + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available in future capability mode + status.display() + model.pprint() + + + Interface Implementation ------------------------ diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 96b87924bf6..d69fecc5837 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -41,14 +41,6 @@ class SolverBase(abc.ABC): """ Base class upon which direct solver interfaces can be built. - - This base class contains the required methods for all direct solvers: - - available: Determines whether the solver is able to be run, combining - both whether it can be found on the system and if the license is valid. - - config: The configuration method for solver objects. - - solve: The main method of every solver - - version: The version of the solver - - is_persistent: Set to false for all direct solvers. """ CONFIG = SolverConfig() From 0b7857475de8741196191e34147760175649957e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 13:21:48 -0700 Subject: [PATCH 0330/1178] Apply black to doc changes --- pyomo/contrib/solver/base.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 98663d85501..1cd9db2baa9 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -46,11 +46,11 @@ class SolverBase(abc.ABC): - version: The version of the solver - is_persistent: Set to false for all non-persistent solvers. - Additionally, solvers should have a :attr:`config` attribute that - inherits from one of :class:`SolverConfig`, - :class:`BranchAndBoundConfig`, - :class:`PersistentSolverConfig`, or - :class:`PersistentBranchAndBoundConfig`. + Additionally, solvers should have a :attr:`config` attribute that + inherits from one of :class:`SolverConfig`, + :class:`BranchAndBoundConfig`, + :class:`PersistentSolverConfig`, or + :class:`PersistentBranchAndBoundConfig`. """ CONFIG = SolverConfig() @@ -105,9 +105,7 @@ def __str__(self): return self.name @abc.abstractmethod - def solve( - self, model: _BlockData, **kwargs - ) -> Results: + def solve(self, model: _BlockData, **kwargs) -> Results: """ Solve a Pyomo model. From 928018003a010a31a36afe62c822361d27569d32 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 13:44:09 -0700 Subject: [PATCH 0331/1178] Switch APPSI/contrib.solver registrations to use "local" (unqualified) names for solvers --- pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/appsi/plugins.py | 10 +++++----- pyomo/contrib/solver/factory.py | 7 +++++-- pyomo/contrib/solver/plugins.py | 10 ++++++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index e6186eeedd2..941883ab997 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1685,7 +1685,7 @@ def decorator(cls): class LegacySolver(LegacySolverInterface, cls): pass - LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register('appsi_' + name, doc)(LegacySolver) return cls diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 5333158239e..cec95337a9b 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -7,17 +7,17 @@ def load(): ExtensionBuilderFactory.register('appsi')(AppsiBuilder) SolverFactory.register( - name='appsi_gurobi', doc='Automated persistent interface to Gurobi' + name='gurobi', doc='Automated persistent interface to Gurobi' )(Gurobi) SolverFactory.register( - name='appsi_cplex', doc='Automated persistent interface to Cplex' + name='cplex', doc='Automated persistent interface to Cplex' )(Cplex) SolverFactory.register( - name='appsi_ipopt', doc='Automated persistent interface to Ipopt' + name='ipopt', doc='Automated persistent interface to Ipopt' )(Ipopt) SolverFactory.register( - name='appsi_cbc', doc='Automated persistent interface to Cbc' + name='cbc', doc='Automated persistent interface to Cbc' )(Cbc) SolverFactory.register( - name='appsi_highs', doc='Automated persistent interface to Highs' + name='highs', doc='Automated persistent interface to Highs' )(Highs) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index e499605afd4..cdd042f9e78 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -16,7 +16,10 @@ class SolverFactoryClass(Factory): - def register(self, name, doc=None): + def register(self, name, legacy_name=None, doc=None): + if legacy_name is None: + legacy_name = name + def decorator(cls): self._cls[name] = cls self._doc[name] = doc @@ -24,7 +27,7 @@ def decorator(cls): class LegacySolver(LegacySolverWrapper, cls): pass - LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register(legacy_name, doc)(LegacySolver) return cls diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index e66818482b4..7d984d10eaa 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -16,7 +16,9 @@ def load(): - SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( - ipopt - ) - SolverFactory.register(name='gurobi_v2', doc='New interface to Gurobi')(Gurobi) + SolverFactory.register( + name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)' + )(ipopt) + SolverFactory.register( + name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' + )(Gurobi) From 316fb3faa4c0faa37a095eceddd60b36957e9ee4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 13:45:37 -0700 Subject: [PATCH 0332/1178] Add __future__ mechanism for switching solver factories --- pyomo/__future__.py | 69 +++++++++++++++++++++++++++++++++ pyomo/contrib/solver/factory.py | 2 +- pyomo/opt/base/solvers.py | 4 ++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 pyomo/__future__.py diff --git a/pyomo/__future__.py b/pyomo/__future__.py new file mode 100644 index 00000000000..7028265b2ad --- /dev/null +++ b/pyomo/__future__.py @@ -0,0 +1,69 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as _environ + + +def __getattr__(name): + if name in ('solver_factory_v1', 'solver_factory_v2', 'solver_factory_v3'): + return solver_factory(int(name[-1])) + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +def solver_factory(version=None): + """Get (or set) the active implementation of the SolverFactory + + This allows users to query / set the current implementation of the + SolverFactory that should be used throughout Pyomo. Valid options are: + + 1: the original Pyomo SolverFactor + 2: the SolverFactory from APPSI + 3: the SolverFactory from pyomo.contrib.solver + + """ + import pyomo.opt.base.solvers as _solvers + import pyomo.contrib.solver.factory as _contrib + import pyomo.contrib.appsi.base as _appsi + versions = { + 1: _solvers.LegacySolverFactory, + 2: _appsi.SolverFactory, + 3: _contrib.SolverFactory, + } + + current = getattr(solver_factory, '_active_version', None) + # First time through, _active_version is not defined. Go look and + # see what it was initialized to in pyomo.environ + if current is None: + for ver, cls in versions.items(): + if cls._cls is _environ.SolverFactory._cls: + solver_factory._active_version = ver + break + return solver_factory._active_version + # + # The user is just asking what the current SolverFactory is; tell them. + if version is None: + return solver_factory._active_version + # + # Update the current SolverFactory to be a shim around (shallow copy + # of) the new active factory + src = versions.get(version, None) + if version is not None: + solver_factory._active_version = version + for attr in ('_description', '_cls', '_doc'): + setattr(_environ.SolverFactory, attr, getattr(src, attr)) + else: + raise ValueError( + "Invalid value for target solver factory version; expected {1, 2, 3}, " + f"received {version}" + ) + return src + +solver_factory._active_version = solver_factory() diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index cdd042f9e78..73666ff57e4 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.opt.base import LegacySolverFactory from pyomo.common.factory import Factory from pyomo.contrib.solver.base import LegacySolverWrapper diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index b11e6393b02..439dda55b57 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -181,7 +181,11 @@ def __call__(self, _name=None, **kwds): return opt +LegacySolverFactory = SolverFactoryClass('solver type') + SolverFactory = SolverFactoryClass('solver type') +SolverFactory._cls = LegacySolverFactory._cls +SolverFactory._doc = LegacySolverFactory._doc # From 6cfbd21615ce8b82f15a0545e187d8a4d2c5e89a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 14:12:31 -0700 Subject: [PATCH 0333/1178] Add documentation (and fix an import) --- doc/OnlineDocs/developer_reference/index.rst | 1 + pyomo/__future__.py | 46 +++++++++++++++++++- pyomo/contrib/solver/factory.py | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/index.rst b/doc/OnlineDocs/developer_reference/index.rst index 0f0f636abee..0feb33cdab9 100644 --- a/doc/OnlineDocs/developer_reference/index.rst +++ b/doc/OnlineDocs/developer_reference/index.rst @@ -12,4 +12,5 @@ scripts using Pyomo. config.rst deprecation.rst expressions/index.rst + future.rst solvers.rst diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 7028265b2ad..c614bf6cc04 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -11,6 +11,25 @@ import pyomo.environ as _environ +__doc__ = """ +Preview capabilities through `pyomo.__future__` +=============================================== + +This module provides a uniform interface for gaining access to future +("preview") capabilities that are either slightly incompatible with the +current official offering, or are still under development with the +intent to replace the current offering. + +Currently supported `__future__` offerings include: + +.. autosummary:: + + solver_factory + +.. autofunction:: solver_factory + +""" + def __getattr__(name): if name in ('solver_factory_v1', 'solver_factory_v2', 'solver_factory_v3'): @@ -23,15 +42,39 @@ def solver_factory(version=None): This allows users to query / set the current implementation of the SolverFactory that should be used throughout Pyomo. Valid options are: - + 1: the original Pyomo SolverFactor 2: the SolverFactory from APPSI 3: the SolverFactory from pyomo.contrib.solver + The current active version can be obtained by calling the method + with no arguments + + .. doctest:: + + >>> from pyomo.__future__ import solver_factory + >>> solver_factory() + 1 + + The active factory can be set either by passing the appropriate + version to this function: + + .. doctest:: + + >>> solver_factory(3) + + + or by importing the "special" name: + + .. doctest:: + + >>> from pyomo.__future__ import solver_factory_v3 + """ import pyomo.opt.base.solvers as _solvers import pyomo.contrib.solver.factory as _contrib import pyomo.contrib.appsi.base as _appsi + versions = { 1: _solvers.LegacySolverFactory, 2: _appsi.SolverFactory, @@ -66,4 +109,5 @@ def solver_factory(version=None): ) return src + solver_factory._active_version = solver_factory() diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 73666ff57e4..52fd9e51236 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from pyomo.opt.base import LegacySolverFactory +from pyomo.opt.base.solvers import LegacySolverFactory from pyomo.common.factory import Factory from pyomo.contrib.solver.base import LegacySolverWrapper From 2c471e43e4e2194a4cdeb9c1b7f7870ee7c19a59 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 15 Feb 2024 16:39:22 -0500 Subject: [PATCH 0334/1178] Simplify argument resolution --- pyomo/contrib/pyros/config.py | 91 ------------------------ pyomo/contrib/pyros/pyros.py | 23 +++--- pyomo/contrib/pyros/tests/test_config.py | 66 ----------------- pyomo/contrib/pyros/tests/test_grcs.py | 56 +++++---------- 4 files changed, 24 insertions(+), 212 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 798e68b157f..749152f234c 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -950,94 +950,3 @@ def pyros_config(): ) return CONFIG - - -def resolve_keyword_arguments(prioritized_kwargs_dicts, func=None): - """ - Resolve the keyword arguments to a callable in the event - the arguments may have been passed in one or more possible - ways. - - A warning-level message is logged (through the default PyROS - logger) in the event an argument is specified in more than one - way. In this case, the value provided through the means with - the highest priority is selected. - - Parameters - ---------- - prioritized_kwargs_dicts : dict - Each entry maps a str to a dict of the keyword arguments - passed via the means described by the str. - Entries of `prioritized_kwargs_dicts` are taken to be - provided in descending order of priority of the means - by which the arguments may have been passed to the callable. - func : callable or None, optional - Callable to which the keyword arguments are/were passed. - Currently, only the `__name__` attribute is used, - for the purpose of logging warning-level messages. - If `None` is passed, then the warning messages - logged are slightly less informative. - - Returns - ------- - resolved_kwargs : dict - Resolved keyword arguments. - """ - # warnings are issued through logger object - default_logger = default_pyros_solver_logger - - # used for warning messages - func_desc = f"passed to {func.__name__}()" if func is not None else "passed" - - # we will loop through the priority dict. initialize: - # - resolved keyword arguments, taking into account the - # priority order and overlap - # - kwarg dicts already processed - # - sequence of kwarg dicts yet to be processed - resolved_kwargs = dict() - prev_prioritized_kwargs_dicts = dict() - remaining_kwargs_dicts = prioritized_kwargs_dicts.copy() - for curr_desc, curr_kwargs in remaining_kwargs_dicts.items(): - overlapping_args = dict() - overlapping_args_set = set() - - for prev_desc, prev_kwargs in prev_prioritized_kwargs_dicts.items(): - # determine overlap between current and previous - # set of kwargs, and remove overlap of current - # and higher priority sets from the result - curr_prev_overlapping_args = ( - set(curr_kwargs.keys()) & set(prev_kwargs.keys()) - ) - overlapping_args_set - if curr_prev_overlapping_args: - # if there is overlap, prepare overlapping args - # for when warning is to be issued - overlapping_args[prev_desc] = curr_prev_overlapping_args - - # update set of args overlapping with higher priority dicts - overlapping_args_set |= curr_prev_overlapping_args - - # ensure kwargs specified in higher priority - # dicts are not overwritten in resolved kwargs - resolved_kwargs.update( - { - kw: val - for kw, val in curr_kwargs.items() - if kw not in overlapping_args_set - } - ) - - # if there are overlaps, log warnings accordingly - # per priority level - for overlap_desc, args_set in overlapping_args.items(): - new_overlapping_args_str = ", ".join(f"{arg!r}" for arg in args_set) - default_logger.warning( - f"Arguments [{new_overlapping_args_str}] passed {curr_desc} " - f"already {func_desc} {overlap_desc}, " - "and will not be overwritten. " - "Consider modifying your arguments to remove the overlap." - ) - - # increment sequence of kwarg dicts already processed - prev_prioritized_kwargs_dicts[curr_desc] = curr_kwargs - - return resolved_kwargs diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0659ab43a64..314b0c3eac4 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -20,7 +20,7 @@ from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.config import pyros_config, resolve_keyword_arguments +from pyomo.contrib.pyros.config import pyros_config from pyomo.contrib.pyros.util import ( recast_to_min_obj, add_decision_rule_constraints, @@ -267,22 +267,15 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): ---- This method can be broken down into three steps: - 1. Resolve user arguments based on how they were passed - and order of precedence of the various means by which - they could be passed. - 2. Cast resolved arguments to ConfigDict. Argument-wise + 1. Cast arguments to ConfigDict. Argument-wise validation is performed automatically. - 3. Inter-argument validation. + Note that arguments specified directly take + precedence over arguments specified indirectly + through direct argument 'options'. + 2. Inter-argument validation. """ - options_dict = kwds.pop("options", {}) - resolved_kwds = resolve_keyword_arguments( - prioritized_kwargs_dicts={ - "explicitly": kwds, - "implicitly through argument 'options'": options_dict, - }, - func=self.solve, - ) - config = self.CONFIG(resolved_kwds) + config = self.CONFIG(kwds.pop("options", {})) + config = config(kwds) state_vars = validate_pyros_inputs(model, config) return config, state_vars diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index eaed462a9b3..cc6fde225f3 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -19,7 +19,6 @@ PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, - resolve_keyword_arguments, SolverIterable, SolverResolvable, UncertaintySetDomain, @@ -723,70 +722,5 @@ def test_logger_type(self): standardizer_func(2) -class TestResolveKeywordArguments(unittest.TestCase): - """ - Test keyword argument resolution function works as expected. - """ - - def test_resolve_kwargs_simple_dict(self): - """ - Test resolve kwargs works, simple example - where there is overlap. - """ - explicit_kwargs = dict(arg1=1) - implicit_kwargs_1 = dict(arg1=2, arg2=3) - implicit_kwargs_2 = dict(arg1=4, arg2=4, arg3=5) - - # expected answer - expected_resolved_kwargs = dict(arg1=1, arg2=3, arg3=5) - - # attempt kwargs resolve - with LoggingIntercept(level=logging.WARNING) as LOG: - resolved_kwargs = resolve_keyword_arguments( - prioritized_kwargs_dicts={ - "explicitly": explicit_kwargs, - "implicitly through set 1": implicit_kwargs_1, - "implicitly through set 2": implicit_kwargs_2, - } - ) - - # check kwargs resolved as expected - self.assertEqual( - resolved_kwargs, - expected_resolved_kwargs, - msg="Resolved kwargs do not match expected value.", - ) - - # extract logger warning messages - warning_msgs = LOG.getvalue().split("\n")[:-1] - - self.assertEqual( - len(warning_msgs), 3, msg="Number of warning messages is not as expected." - ) - - # check contents of warning msgs - self.assertRegex( - warning_msgs[0], - expected_regex=( - r"Arguments \['arg1'\] passed implicitly through set 1 " - r"already passed explicitly.*" - ), - ) - self.assertRegex( - warning_msgs[1], - expected_regex=( - r"Arguments \['arg1'\] passed implicitly through set 2 " - r"already passed explicitly.*" - ), - ) - self.assertRegex( - warning_msgs[2], - expected_regex=( - r"Arguments \['arg2'\] passed implicitly through set 2 " - r"already passed implicitly through set 1.*" - ), - ) - - if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index a94b4d9d408..59045f3c6b7 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6409,46 +6409,22 @@ def test_pyros_kwargs_with_overlap(self): global_subsolver = SolverFactory("baron") # Call the PyROS solver - with LoggingIntercept(level=logging.WARNING) as LOG: - results = pyros_solver.solve( - model=m, - first_stage_variables=[m.x1, m.x2], - second_stage_variables=[], - uncertain_params=[m.u1, m.u2], - uncertainty_set=ellipsoid, - local_solver=local_subsolver, - global_solver=global_subsolver, - bypass_local_separation=True, - solve_master_globally=True, - options={ - "objective_focus": ObjectiveType.worst_case, - "solve_master_globally": False, - "max_iter": 1, - "time_limit": 1000, - }, - ) - - # extract warning-level messages. - warning_msgs = LOG.getvalue().split("\n")[:-1] - resolve_kwargs_warning_msgs = [ - msg - for msg in warning_msgs - if msg.startswith("Arguments [") - and "Consider modifying your arguments" in msg - ] - self.assertEqual( - len(resolve_kwargs_warning_msgs), - 1, - msg="Number of warning-level messages not as expected.", - ) - - self.assertRegex( - resolve_kwargs_warning_msgs[0], - expected_regex=( - r"Arguments \['solve_master_globally'\] passed " - r"implicitly through argument 'options' " - r"already passed .*explicitly.*" - ), + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u1, m.u2], + uncertainty_set=ellipsoid, + local_solver=local_subsolver, + global_solver=global_subsolver, + bypass_local_separation=True, + solve_master_globally=True, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": False, + "max_iter": 1, + "time_limit": 1000, + }, ) # check termination status as expected From d2a3ff14e9d70d118a18a5dca4c728185e8e0a24 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:06:02 -0700 Subject: [PATCH 0335/1178] Update documentation; include package needed for sphinx enum tools --- doc/OnlineDocs/developer_reference/solvers.rst | 3 +-- pyomo/contrib/solver/base.py | 13 +++++++++++-- pyomo/contrib/solver/config.py | 2 +- setup.py | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 78344293e39..581d899af50 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -75,7 +75,6 @@ Future Capability Mode model.pprint() - Interface Implementation ------------------------ @@ -88,7 +87,7 @@ All solvers should have the following: .. autoclass:: pyomo.contrib.solver.base.SolverBase :members: -Persistent solvers should also include: +Persistent solvers include additional members as well as other configuration options: .. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase :show-inheritance: diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 1cd9db2baa9..327ad2e01ca 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -19,7 +19,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.timing import HierarchicalTimer +from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning from pyomo.opt.results.results_ import SolverResults as LegacySolverResults @@ -28,7 +28,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.config import SolverConfig, PersistentSolverConfig from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( Results, @@ -104,6 +104,7 @@ def __str__(self): # preserve the previous behavior return self.name + @document_kwargs_from_configdict(CONFIG) @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: """ @@ -176,6 +177,14 @@ class PersistentSolverBase(SolverBase): Example usage can be seen in the Gurobi interface. """ + CONFIG = PersistentSolverConfig() + + def __init__(self, kwds): + super().__init__(kwds) + + @document_kwargs_from_configdict(CONFIG) + def solve(self, model: _BlockData, **kwargs) -> Results: + super().solve(model, kwargs) def is_persistent(self): """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index a1133f93ae4..335307c1bbf 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -64,7 +64,7 @@ def __init__( domain=str, default=None, description="The directory in which generated files should be saved. " - "This replaced the `keepfiles` option.", + "This replaces the `keepfiles` option.", ), ) self.load_solutions: bool = self.declare( diff --git a/setup.py b/setup.py index e2d702db010..27d169af746 100644 --- a/setup.py +++ b/setup.py @@ -253,6 +253,7 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', + 'enum-tools[sphinx]', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], From 75da70e77d245866cf865ae7e8fd997769441566 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:16:13 -0700 Subject: [PATCH 0336/1178] Fix init; add more descriptive skip messages --- pyomo/contrib/solver/base.py | 5 +- .../solver/tests/solvers/test_solvers.py | 76 +++++++++---------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 327ad2e01ca..bc4ab725a81 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -179,10 +179,11 @@ class PersistentSolverBase(SolverBase): """ CONFIG = PersistentSolverConfig() - def __init__(self, kwds): - super().__init__(kwds) + def __init__(self, **kwds): + super().__init__(**kwds) @document_kwargs_from_configdict(CONFIG) + @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: super().solve(model, kwargs) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 36f3596e890..0393d1adb2e 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -55,7 +55,7 @@ def test_remove_variable_and_objective( # this test is for issue #2888 opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) @@ -75,7 +75,7 @@ def test_remove_variable_and_objective( def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -116,7 +116,7 @@ def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.obj = pe.Objective(expr=m.x) @@ -137,7 +137,7 @@ def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var(bounds=(-2, 2)) @@ -159,7 +159,7 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) @@ -179,7 +179,7 @@ def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): def test_param_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -219,7 +219,7 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): """ opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -255,7 +255,7 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): def test_equality(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') if isinstance(opt, ipopt): opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() @@ -293,7 +293,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase]): def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -331,7 +331,7 @@ def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): def test_no_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -362,7 +362,7 @@ def test_no_objective(self, name: str, opt_class: Type[SolverBase]): def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -416,7 +416,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -465,7 +465,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): def test_duals(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -490,7 +490,7 @@ def test_mutable_quadratic_coefficient( ): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -512,7 +512,7 @@ def test_mutable_quadratic_coefficient( def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -542,7 +542,7 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): treat_fixed_vars_as_params ) if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -580,7 +580,7 @@ def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -618,7 +618,7 @@ def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -634,7 +634,7 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -652,7 +652,7 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') try: import numpy as np except: @@ -746,7 +746,7 @@ def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.y = pe.Var(bounds=(-1, None)) m.obj = pe.Objective(expr=m.y) @@ -792,7 +792,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): def test_exp(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -806,7 +806,7 @@ def test_exp(self, name: str, opt_class: Type[SolverBase]): def test_log(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(initialize=1) m.y = pe.Var() @@ -820,7 +820,7 @@ def test_log(self, name: str, opt_class: Type[SolverBase]): def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -848,7 +848,7 @@ def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.y = pe.Var() m.p = pe.Param(mutable=True) @@ -880,7 +880,7 @@ def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None)) m.y = pe.Var() @@ -930,7 +930,7 @@ def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): def test_time_limit(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') from sys import platform if platform == 'win32': @@ -986,7 +986,7 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase]): def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -1050,7 +1050,7 @@ def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): def test_domain(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) @@ -1074,7 +1074,7 @@ def test_domain(self, name: str, opt_class: Type[SolverBase]): def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) @@ -1098,7 +1098,7 @@ def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(domain=pe.Binary) m.y = pe.Var() @@ -1125,7 +1125,7 @@ def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-10, 10)) @@ -1156,7 +1156,7 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() @@ -1183,7 +1183,7 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() @@ -1218,7 +1218,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): def test_bug_1(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(3, 7)) @@ -1246,7 +1246,7 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase]): for fixed_var_option in [True, False]: opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option @@ -1272,7 +1272,7 @@ class TestLegacySolverInterface(unittest.TestCase): def test_param_updates(self, name: str, opt_class: Type[SolverBase]): opt = pe.SolverFactory(name + '_v2') if not opt.available(exception_flag=False): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -1302,7 +1302,7 @@ def test_param_updates(self, name: str, opt_class: Type[SolverBase]): def test_load_solutions(self, name: str, opt_class: Type[SolverBase]): opt = pe.SolverFactory(name + '_v2') if not opt.available(exception_flag=False): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.obj = pe.Objective(expr=m.x) From e7eb1423272e53e984d7ae3ea8541eded92b56e7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:16:36 -0700 Subject: [PATCH 0337/1178] Apply blacl --- pyomo/contrib/solver/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index bc4ab725a81..09c73ab3a9b 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -177,6 +177,7 @@ class PersistentSolverBase(SolverBase): Example usage can be seen in the Gurobi interface. """ + CONFIG = PersistentSolverConfig() def __init__(self, **kwds): From bf4f27018d4948cefc0f44f4e3c39d5ff3848eca Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:35:36 -0700 Subject: [PATCH 0338/1178] Small typo; changes enum-tools line --- pyomo/contrib/solver/config.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 335307c1bbf..d36c7102620 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -89,7 +89,7 @@ def __init__( ConfigValue( domain=bool, default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components." + description="If True, the names given to the solver will reflect the names of the Pyomo components. " "Cannot be changed after set_instance is called.", ), ) diff --git a/setup.py b/setup.py index 27d169af746..1572910ad89 100644 --- a/setup.py +++ b/setup.py @@ -253,7 +253,7 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', - 'enum-tools[sphinx]', + 'enum-tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], From eb9b2532cd4045ca0b674a6c5b73503258731b9a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:49:19 -0700 Subject: [PATCH 0339/1178] Underscore instead of dash --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1572910ad89..1f0d56c10a7 100644 --- a/setup.py +++ b/setup.py @@ -253,7 +253,7 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', - 'enum-tools', + 'enum_tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], From f313b0a49c1d8bbb92244029487c8ce57d5e3977 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 15:51:09 -0700 Subject: [PATCH 0340/1178] NFC: apply black --- pyomo/contrib/appsi/plugins.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index cec95337a9b..a765f9a45de 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -9,15 +9,13 @@ def load(): SolverFactory.register( name='gurobi', doc='Automated persistent interface to Gurobi' )(Gurobi) - SolverFactory.register( - name='cplex', doc='Automated persistent interface to Cplex' - )(Cplex) - SolverFactory.register( - name='ipopt', doc='Automated persistent interface to Ipopt' - )(Ipopt) - SolverFactory.register( - name='cbc', doc='Automated persistent interface to Cbc' - )(Cbc) - SolverFactory.register( - name='highs', doc='Automated persistent interface to Highs' - )(Highs) + SolverFactory.register(name='cplex', doc='Automated persistent interface to Cplex')( + Cplex + ) + SolverFactory.register(name='ipopt', doc='Automated persistent interface to Ipopt')( + Ipopt + ) + SolverFactory.register(name='cbc', doc='Automated persistent interface to Cbc')(Cbc) + SolverFactory.register(name='highs', doc='Automated persistent interface to Highs')( + Highs + ) From 74971722ca3bd9a8e1aa3d2d8549373a77a0ba6f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 15:58:03 -0700 Subject: [PATCH 0341/1178] NFC: update copyright on new file (missed by #3139) --- pyomo/__future__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index c614bf6cc04..235143592f1 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From cfbab706bd472a37f70830d3a3f8371f1be1ada0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:59:22 -0700 Subject: [PATCH 0342/1178] Add in two more deps --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 1f0d56c10a7..dbb5a68ceb2 100644 --- a/setup.py +++ b/setup.py @@ -253,6 +253,8 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', + 'sphinx-toolbox>=2.16.0', + 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero From 0330e095f681d368a0bb50213bf4347575248b6b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 16:06:03 -0700 Subject: [PATCH 0343/1178] NFC: fix doc formatting --- pyomo/__future__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 235143592f1..0dc22cca0a7 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -43,9 +43,9 @@ def solver_factory(version=None): This allows users to query / set the current implementation of the SolverFactory that should be used throughout Pyomo. Valid options are: - 1: the original Pyomo SolverFactor - 2: the SolverFactory from APPSI - 3: the SolverFactory from pyomo.contrib.solver + - ``1``: the original Pyomo SolverFactor + - ``2``: the SolverFactory from APPSI + - ``3``: the SolverFactory from pyomo.contrib.solver The current active version can be obtained by calling the method with no arguments From b3c4b66bf0b78aa8a69d7bb30f9c7991dee1f147 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 16:07:55 -0700 Subject: [PATCH 0344/1178] Add missing doc file --- doc/OnlineDocs/developer_reference/future.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/OnlineDocs/developer_reference/future.rst diff --git a/doc/OnlineDocs/developer_reference/future.rst b/doc/OnlineDocs/developer_reference/future.rst new file mode 100644 index 00000000000..531c0fdb5c6 --- /dev/null +++ b/doc/OnlineDocs/developer_reference/future.rst @@ -0,0 +1,3 @@ + +.. automodule:: pyomo.__future__ + :noindex: From f45201a3209a52979f43168c2af5ddaa36bf3ab6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 16:15:41 -0700 Subject: [PATCH 0345/1178] NFC: additional doc formatting --- pyomo/__future__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 0dc22cca0a7..a2e08ccf291 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -12,15 +12,15 @@ import pyomo.environ as _environ __doc__ = """ -Preview capabilities through `pyomo.__future__` -=============================================== +Preview capabilities through ``pyomo.__future__`` +================================================= This module provides a uniform interface for gaining access to future ("preview") capabilities that are either slightly incompatible with the current official offering, or are still under development with the intent to replace the current offering. -Currently supported `__future__` offerings include: +Currently supported ``__future__`` offerings include: .. autosummary:: From 7965ac52b8b88bf14a0f8bac32b2f6c20d6eabce Mon Sep 17 00:00:00 2001 From: kaklise Date: Thu, 15 Feb 2024 15:27:31 -0800 Subject: [PATCH 0346/1178] removed group_data function --- pyomo/contrib/parmest/parmest.py | 39 -------------------------------- 1 file changed, 39 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 2e44b278423..83b24e39327 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -16,7 +16,6 @@ # TODO: move use_mpisppy to a Pyomo configuration option # Redesign TODOS -# TODO: remove group_data,this is only used in 1 example and should be handled by the user in Experiment # TODO: _treemaker is not used in parmest, the code could be moved to scenario tree if needed # TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) # TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) @@ -272,44 +271,6 @@ def _experiment_instance_creation_callback( # return m - -# def group_data(data, groupby_column_name, use_mean=None): -# """ -# Group data by scenario - -# Parameters -# ---------- -# data: DataFrame -# Data -# groupby_column_name: strings -# Name of data column which contains scenario numbers -# use_mean: list of column names or None, optional -# Name of data columns which should be reduced to a single value per -# scenario by taking the mean - -# Returns -# ---------- -# grouped_data: list of dictionaries -# Grouped data -# """ -# if use_mean is None: -# use_mean_list = [] -# else: -# use_mean_list = use_mean - -# grouped_data = [] -# for exp_num, group in data.groupby(data[groupby_column_name]): -# d = {} -# for col in group.columns: -# if col in use_mean_list: -# d[col] = group[col].mean() -# else: -# d[col] = list(group[col]) -# grouped_data.append(d) - -# return grouped_data - - def SSE(model): expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr From 9157b8c048a26aef8b3637f979a16d01c3768cf1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 16:29:59 -0700 Subject: [PATCH 0347/1178] Update documentation to reflect the new preview page --- .../developer_reference/solvers.rst | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 581d899af50..db9fe307b18 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -13,20 +13,23 @@ New Interface Usage ------------------- The new interfaces have two modes: backwards compatible and future capability. -To use the backwards compatible version, simply use the ``SolverFactory`` -as usual and replace the solver name with the new version. Currently, the new -versions available are: +The future capability mode can be accessed directly or by switching the default +``SolverFactory`` version (see :doc:`future`). Currently, the new versions +available are: .. list-table:: Available Redesigned Solvers - :widths: 25 25 + :widths: 25 25 25 :header-rows: 1 * - Solver - - ``SolverFactory`` Name + - ``SolverFactory``([1]) Name + - ``SolverFactory``([3]) Name * - ipopt - ``ipopt_v2`` - * - GUROBI + - ``ipopt`` + * - Gurobi - ``gurobi_v2`` + - ``gurobi`` Backwards Compatible Mode ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -52,8 +55,12 @@ Backwards Compatible Mode Future Capability Mode ^^^^^^^^^^^^^^^^^^^^^^ +There are multiple ways to utilize the future compatibility mode: direct import +or changed ``SolverFactory`` version. + .. code-block:: python + # Direct import import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.contrib.solver.ipopt import ipopt @@ -74,6 +81,29 @@ Future Capability Mode status.display() model.pprint() +Changing the ``SolverFactory`` version: + +.. code-block:: python + + # Change SolverFactory version + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.__future__ import solver_factory_v3 + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt').solve(model) + assert_optimal_termination(status) + # Displays important results information; only available in future capability mode + status.display() + model.pprint() Interface Implementation ------------------------ From 8e56d4e0acdb36821bd96f39624f88e21f01fd93 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 16:41:52 -0700 Subject: [PATCH 0348/1178] Doc formatting fix --- doc/OnlineDocs/developer_reference/solvers.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index db9fe307b18..f0f2a574331 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -22,8 +22,8 @@ available are: :header-rows: 1 * - Solver - - ``SolverFactory``([1]) Name - - ``SolverFactory``([3]) Name + - ``SolverFactory`` ([1]) Name + - ``SolverFactory`` ([3]) Name * - ipopt - ``ipopt_v2`` - ``ipopt`` From edfc620772a17274afc08cfea78031ec578319ef Mon Sep 17 00:00:00 2001 From: kaklise Date: Thu, 15 Feb 2024 15:57:46 -0800 Subject: [PATCH 0349/1178] Removed group_data from example, data is now grouped using data_i, and mean of sv and caf --- .../reactor_design/timeseries_data_example.py | 54 +++---------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 7ffca3696eb..cde7febf6cc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -23,15 +23,16 @@ class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number - self.data_i = data[experiment_number] + data_i = data.loc[data['experiment'] == experiment_number,:] + self.data_i = data_i.reset_index() self.model = None def finalize_model(self): m = self.model # Experiment inputs values - m.sv = self.data_i['sv'] - m.caf = self.data_i['caf'] + m.sv = self.data_i['sv'].mean() + m.caf = self.data_i['caf'].mean() # Experiment output values m.ca = self.data_i['ca'][0] @@ -42,59 +43,18 @@ def finalize_model(self): return m -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - - def main(): - # Parameter estimation using timeseries data + # Parameter estimation using timeseries data, grouped by experiment number # Data, includes multiple sensors for ca and cc file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) data = pd.read_csv(file_name) - # Group time series data into experiments, return the mean value for sv and caf - # Returns a list of dictionaries - data_ts = group_data(data, 'experiment', ['sv', 'caf']) - # Create an experiment list exp_list = [] - for i in range(len(data_ts)): - exp_list.append(TimeSeriesReactorDesignExperiment(data_ts, i)) + for i in data['experiment'].unique(): + exp_list.append(TimeSeriesReactorDesignExperiment(data, i)) def SSE_timeseries(model): From 2bcb0e074dce33d2aa965e63e1d647fa4aa7403c Mon Sep 17 00:00:00 2001 From: kaklise Date: Thu, 15 Feb 2024 16:04:57 -0800 Subject: [PATCH 0350/1178] Data formatting moved to create_model --- .../rooney_biegler/bootstrap_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../parameter_estimation_example.py | 2 +- .../examples/rooney_biegler/rooney_biegler.py | 12 ++++--- .../rooney_biegler_with_constraint.py | 12 ++++--- pyomo/contrib/parmest/tests/test_parmest.py | 32 +++++++++++-------- 6 files changed, 35 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 953de98a48e..b9ef114c2b3 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -35,7 +35,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index b08d5456982..7799148389a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -36,7 +36,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 9c851ecd9c8..aa810453883 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -35,7 +35,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 8fa0fd70ec6..920bd2987e0 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -52,15 +52,17 @@ def __init__(self, data): self.model = None def create_model(self): - self.model = rooney_biegler_model(self.data) + # rooney_biegler_model expects a dataframe + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_model(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -72,8 +74,8 @@ def finalize_model(self): m = self.model # Experiment output values - m.hour = self.data.iloc[0]['hour'] - m.y = self.data.iloc[0]['y'] + m.hour = self.data['hour'] + m.y = self.data['y'] def get_labeled_model(self): self.create_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 463c876dc43..499f6eb505b 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -56,15 +56,17 @@ def __init__(self, data): self.model = None def create_model(self): - self.model = rooney_biegler_model_with_constraint(self.data) + # rooney_biegler_model_with_constraint expects a dataframe + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_model_with_constraint(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -76,8 +78,8 @@ def finalize_model(self): m = self.model # Experiment output values - m.hour = self.data.iloc[0]['hour'] - m.y = self.data.iloc[0]['y'] + m.hour = self.data['hour'] + m.y = self.data['y'] def get_labeled_model(self): self.create_model() diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index ff8d1663bc9..bbffa982bcf 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -77,7 +77,7 @@ def SSE(model): exp_list = [] for i in range(data.shape[0]): exp_list.append( - RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) + RooneyBieglerExperiment(data.loc[i, :]) ) # Create an instance of the parmest estimator @@ -386,13 +386,14 @@ def response_rule(m, h): class RooneyBieglerExperimentParams(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_params(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_params(data_df) rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_params_exp_list.append( RooneyBieglerExperimentParams( - self.data.loc[i, :].to_frame().transpose() + self.data.loc[i, :] ) ) @@ -422,15 +423,16 @@ def response_rule(m, h): class RooneyBieglerExperimentIndexedParams(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_indexed_params(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_indexed_params(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -439,7 +441,7 @@ def label_model(self): for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( RooneyBieglerExperimentIndexedParams( - self.data.loc[i, :].to_frame().transpose() + self.data.loc[i, :] ) ) @@ -465,12 +467,13 @@ def response_rule(m, h): class RooneyBieglerExperimentVars(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_vars(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_vars(data_df) rooney_biegler_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_vars_exp_list.append( - RooneyBieglerExperimentVars(self.data.loc[i, :].to_frame().transpose()) + RooneyBieglerExperimentVars(self.data.loc[i, :]) ) def rooney_biegler_indexed_vars(data): @@ -501,15 +504,16 @@ def response_rule(m, h): class RooneyBieglerExperimentIndexedVars(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_indexed_vars(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_indexed_vars(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -518,7 +522,7 @@ def label_model(self): for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( RooneyBieglerExperimentIndexedVars( - self.data.loc[i, :].to_frame().transpose() + self.data.loc[i, :] ) ) @@ -985,7 +989,7 @@ def SSE(model): exp_list = [] for i in range(data.shape[0]): exp_list.append( - RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) + RooneyBieglerExperiment(data.loc[i, :]) ) solver_options = {"tol": 1e-8} From ae28ceea6f4b255242d58847e0c92bba5536a87d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 07:29:27 -0700 Subject: [PATCH 0351/1178] Update copyright year on all solver files --- pyomo/contrib/solver/__init__.py | 2 +- pyomo/contrib/solver/base.py | 2 +- pyomo/contrib/solver/config.py | 2 +- pyomo/contrib/solver/factory.py | 2 +- pyomo/contrib/solver/gurobi.py | 2 +- pyomo/contrib/solver/ipopt.py | 2 +- pyomo/contrib/solver/plugins.py | 2 +- pyomo/contrib/solver/results.py | 2 +- pyomo/contrib/solver/sol_reader.py | 2 +- pyomo/contrib/solver/solution.py | 2 +- pyomo/contrib/solver/tests/__init__.py | 2 +- pyomo/contrib/solver/tests/solvers/__init__.py | 2 +- pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py | 2 +- pyomo/contrib/solver/tests/solvers/test_ipopt.py | 2 +- pyomo/contrib/solver/tests/solvers/test_solvers.py | 2 +- pyomo/contrib/solver/tests/unit/__init__.py | 2 +- pyomo/contrib/solver/tests/unit/sol_files/__init__.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 2 +- pyomo/contrib/solver/tests/unit/test_config.py | 2 +- pyomo/contrib/solver/tests/unit/test_results.py | 2 +- pyomo/contrib/solver/tests/unit/test_sol_reader.py | 2 +- pyomo/contrib/solver/tests/unit/test_solution.py | 2 +- pyomo/contrib/solver/tests/unit/test_util.py | 2 +- pyomo/contrib/solver/util.py | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/solver/__init__.py b/pyomo/contrib/solver/__init__.py index e3eafa991cc..2dc73091ea2 100644 --- a/pyomo/contrib/solver/__init__.py +++ b/pyomo/contrib/solver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 09c73ab3a9b..a60e770e660 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index d36c7102620..d13e1caf81d 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 52fd9e51236..91ce92a9dee 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 919e7ae3995..c1b02c08ef9 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index f70cbb5f194..ff809a146c1 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index 7d984d10eaa..cb089200100 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index e80bad126a1..b330773e4f3 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index c4497516de2..2817dab4516 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index d4069b5b5a1..31792a76dfe 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/__init__.py b/pyomo/contrib/solver/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/__init__.py +++ b/pyomo/contrib/solver/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/solvers/__init__.py +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py index d4c0078a0df..f2dd79619b4 100644 --- a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index 627d502629c..2886045055c 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 0393d1adb2e..e5af2ada170 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/unit/__init__.py +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py +++ b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index b8d5c79fc0f..5fecd012cda 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index f28dd5fcedf..354cfd8a37a 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 7b9de32bc00..2d8f6460448 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_sol_reader.py b/pyomo/contrib/solver/tests/unit/test_sol_reader.py index 0ab94dfc4ac..d5602945e07 100644 --- a/pyomo/contrib/solver/tests/unit/test_sol_reader.py +++ b/pyomo/contrib/solver/tests/unit/test_sol_reader.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 7a18344d4cb..a5ee8a9e391 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py index ab8a778067f..f2e8ee707f4 100644 --- a/pyomo/contrib/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index af856eab7e2..d104022692e 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 23bdbf7b76f674b5d437a3136387e28a84844086 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 07:37:30 -0700 Subject: [PATCH 0352/1178] Add hidden doctest to reset solver factory --- pyomo/__future__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index a2e08ccf291..87b1d4e77b3 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -70,6 +70,11 @@ def solver_factory(version=None): >>> from pyomo.__future__ import solver_factory_v3 + .. doctest:: + :hide: + + >>> from pyomo.__future__ import solver_factory_v1 + """ import pyomo.opt.base.solvers as _solvers import pyomo.contrib.solver.factory as _contrib From d405bcb4ed379cd0f61c4dd0f7901019ff742810 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 09:01:05 -0700 Subject: [PATCH 0353/1178] Add missing dep for windows/conda enum_tools --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- doc/OnlineDocs/developer_reference/solvers.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e5513d25975..77f47b505ff 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -75,7 +75,7 @@ jobs: python: 3.9 TARGET: win PYENV: conda - PACKAGES: glpk pytest-qt + PACKAGES: glpk pytest-qt filelock - os: ubuntu-latest python: '3.11' diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index c5028606c17..87d6aa4d7a8 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -76,7 +76,7 @@ jobs: - os: windows-latest TARGET: win PYENV: conda - PACKAGES: glpk pytest-qt + PACKAGES: glpk pytest-qt filelock - os: ubuntu-latest python: '3.11' diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index f0f2a574331..5f6f3fc547b 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -22,8 +22,8 @@ available are: :header-rows: 1 * - Solver - - ``SolverFactory`` ([1]) Name - - ``SolverFactory`` ([3]) Name + - ``SolverFactory`` (v1) Name + - ``SolverFactory`` (v3) Name * - ipopt - ``ipopt_v2`` - ``ipopt`` From 832a789cd0c8858d7c0c6616419288bacf794643 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 11:29:36 -0700 Subject: [PATCH 0354/1178] Add minimal example for presolve and scaling to docs --- .../developer_reference/solvers.rst | 39 +++++++++++++++++++ pyomo/contrib/solver/ipopt.py | 25 +++++++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 5f6f3fc547b..8c8c9e5b8ee 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -105,6 +105,45 @@ Changing the ``SolverFactory`` version: status.display() model.pprint() +Linear Presolve and Scaling +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The new interface will allow for direct manipulation of linear presolve and scaling +options for certain solvers. Currently, these options are only available for +``ipopt``. + +.. autoclass:: pyomo.contrib.solver.ipopt.ipopt + :members: solve + +The ``writer_config`` configuration option can be used to manipulate presolve +and scaling options: + +.. code-block:: python + + >>> from pyomo.contrib.solver.ipopt import ipopt + >>> opt = ipopt() + >>> opt.config.writer_config.display() + + show_section_timing: false + skip_trivial_constraints: true + file_determinism: FileDeterminism.ORDERED + symbolic_solver_labels: false + scale_model: true + export_nonlinear_variables: None + row_order: None + column_order: None + export_defined_variables: true + linear_presolve: true + +Note that, by default, both ``linear_presolve`` and ``scale_model`` are enabled. +Users can manipulate ``linear_presolve`` and ``scale_model`` to their preferred +states by changing their values. + +.. code-block:: python + + >>> opt.config.writer_config.linear_presolve = False + + Interface Implementation ------------------------ diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index ff809a146c1..edea4e693b4 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -17,7 +17,12 @@ from typing import Mapping, Optional, Sequence from pyomo.common import Executable -from pyomo.common.config import ConfigValue, NonNegativeFloat +from pyomo.common.config import ( + ConfigValue, + NonNegativeFloat, + document_kwargs_from_configdict, + ConfigDict, +) from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer @@ -65,11 +70,20 @@ def __init__( visibility=visibility, ) - self.executable = self.declare( - 'executable', ConfigValue(default=Executable('ipopt')) + self.executable: Executable = self.declare( + 'executable', + ConfigValue( + default=Executable('ipopt'), + description="Preferred executable for ipopt. Defaults to searching the " + "``PATH`` for the first available ``ipopt``.", + ), ) - self.writer_config = self.declare( - 'writer_config', ConfigValue(default=NLWriter.CONFIG()) + self.writer_config: ConfigDict = self.declare( + 'writer_config', + ConfigValue( + default=NLWriter.CONFIG(), + description="For the manipulation of NL writer options.", + ), ) @@ -270,6 +284,7 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd.append(str(k) + '=' + str(val)) return cmd + @document_kwargs_from_configdict(CONFIG) def solve(self, model, **kwds): # Begin time tracking start_timestamp = datetime.datetime.now(datetime.timezone.utc) From ec7c8e6a3b417b42880d36528c0d75462594497b Mon Sep 17 00:00:00 2001 From: lukasbiton Date: Fri, 16 Feb 2024 22:12:45 +0000 Subject: [PATCH 0355/1178] error msg more explicit wrt different interfaces --- pyomo/contrib/appsi/solvers/highs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a9a23682355..3612b9d5014 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -680,9 +680,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Highs interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') From 5b74de426559b5c39876e1de38e2721a9e997d92 Mon Sep 17 00:00:00 2001 From: lukasbiton Date: Fri, 16 Feb 2024 22:21:40 +0000 Subject: [PATCH 0356/1178] align new error message for all appsi solvers --- pyomo/contrib/appsi/solvers/cbc.py | 8 +++++--- pyomo/contrib/appsi/solvers/cplex.py | 8 +++++--- pyomo/contrib/appsi/solvers/gurobi.py | 8 +++++--- pyomo/contrib/appsi/solvers/ipopt.py | 8 +++++--- pyomo/contrib/appsi/solvers/wntr.py | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 2c522af864d..7f04ffbfce7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -411,9 +411,11 @@ def _check_and_escape_options(): if cp.returncode != 0: if self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cbc interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) results = Results() diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1d7147f16e8..1b7ab5000d2 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -341,9 +341,11 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loades. ' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cplex interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) else: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index aa233ef77d6..1e18862e3bd 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -946,9 +946,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Gurobi interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index d7a786e6c2c..29e74f81c98 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -421,9 +421,11 @@ def _parse_sol(self): results.best_feasible_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Ipopt interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index e1835b810b0..00c0598c687 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -169,9 +169,11 @@ def _solve(self, timer: HierarchicalTimer): timer.stop('load solution') else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Wntr interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) return results From 120c9a4c8fea118e6fbf5a1a9ace5b93f6ce127f Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 16 Feb 2024 17:22:03 -0500 Subject: [PATCH 0357/1178] Incorporate updated interfaces of `common.config` --- pyomo/contrib/pyros/config.py | 59 +---------- pyomo/contrib/pyros/tests/test_config.py | 119 ----------------------- pyomo/contrib/pyros/uncertainty_sets.py | 42 -------- 3 files changed, 4 insertions(+), 216 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 749152f234c..a7ca41d095f 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -4,13 +4,13 @@ from collections.abc import Iterable import logging -import os from pyomo.common.collections import ComponentSet from pyomo.common.config import ( ConfigDict, ConfigValue, In, + IsInstance, NonNegativeFloat, InEnum, Path, @@ -20,7 +20,7 @@ from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger -from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain +from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet default_pyros_solver_logger = setup_pyros_logger() @@ -93,57 +93,6 @@ def domain_name(self): return "positive int or -1" -class PathLikeOrNone: - """ - Validator for path-like objects. - - This interface is a wrapper around the domain validator - ``common.config.Path``, and extends the domain of interest to - to include: - - None - - objects following the Python ``os.PathLike`` protocol. - - Parameters - ---------- - **config_path_kwargs : dict - Keyword arguments to ``common.config.Path``. - """ - - def __init__(self, **config_path_kwargs): - """Initialize self (see class docstring).""" - self.config_path = Path(**config_path_kwargs) - - def __call__(self, path): - """ - Cast path to expanded string representation. - - Parameters - ---------- - path : None str, bytes, or path-like - Object to be cast. - - Returns - ------- - None - If obj is None. - str - String representation of path-like object. - """ - if path is None: - return path - - # prevent common.config.Path from invoking - # str() on the path-like object - path_str = os.fsdecode(path) - - # standardize path str as necessary - return self.config_path(path_str) - - def domain_name(self): - """Return str briefly describing domain encompassed by self.""" - return "str, bytes, path-like or None" - - def mutable_param_validator(param_obj): """ Check that Param-like object has attribute `mutable=True`. @@ -637,7 +586,7 @@ def pyros_config(): "uncertainty_set", ConfigValue( default=None, - domain=UncertaintySetDomain(), + domain=IsInstance(UncertaintySet), description=( """ Uncertainty set against which the @@ -871,7 +820,7 @@ def pyros_config(): "subproblem_file_directory", ConfigValue( default=None, - domain=PathLikeOrNone(), + domain=Path(), description=( """ Directory to which to export subproblems not successfully diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index cc6fde225f3..76b9114b9e6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -3,11 +3,9 @@ """ import logging -import os import unittest from pyomo.core.base import ConcreteModel, Var, _VarData -from pyomo.common.config import Path from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, _ParamData @@ -16,12 +14,10 @@ mutable_param_validator, LoggerType, SolverNotResolvable, - PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, SolverIterable, SolverResolvable, - UncertaintySetDomain, ) from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults @@ -275,34 +271,6 @@ def test_standardizer_valid_mutable_params(self): ) -class TestUncertaintySetDomain(unittest.TestCase): - """ - Test domain validator for uncertainty set arguments. - """ - - @unittest.skipUnless(numpy_available, "Numpy is not available.") - def test_uncertainty_set_domain_valid_set(self): - """ - Test validator works for valid argument. - """ - standardizer_func = UncertaintySetDomain() - bset = BoxSet([[0, 1]]) - self.assertIs( - bset, - standardizer_func(bset), - msg="Output of uncertainty set domain not as expected.", - ) - - def test_uncertainty_set_domain_invalid_type(self): - """ - Test validator works for valid argument. - """ - standardizer_func = UncertaintySetDomain() - exc_str = "Expected an .*UncertaintySet object.*received object 2" - with self.assertRaisesRegex(ValueError, exc_str): - standardizer_func(2) - - AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" @@ -580,93 +548,6 @@ def test_config_objective_focus(self): config.objective_focus = invalid_focus -class TestPathLikeOrNone(unittest.TestCase): - """ - Test interface for validating path-like arguments. - """ - - def test_none_valid(self): - """ - Test `None` is valid. - """ - standardizer_func = PathLikeOrNone() - - self.assertIs( - standardizer_func(None), - None, - msg="Output of `PathLikeOrNone` standardizer not as expected.", - ) - - def test_str_bytes_path_like_valid(self): - """ - Check path-like validator handles str, bytes, and path-like - inputs correctly. - """ - - class ExamplePathLike(os.PathLike): - """ - Path-like class for testing. Key feature: __fspath__ - and __str__ return different outputs. - """ - - def __init__(self, path_str_or_bytes): - self.path = path_str_or_bytes - - def __fspath__(self): - return self.path - - def __str__(self): - path_str = os.fsdecode(self.path) - return f"{type(self).__name__}({path_str})" - - path_standardization_func = PathLikeOrNone() - - # construct path arguments of different type - path_as_str = "example_output_dir/" - path_as_bytes = os.fsencode(path_as_str) - path_like_from_str = ExamplePathLike(path_as_str) - path_like_from_bytes = ExamplePathLike(path_as_bytes) - - # for all possible arguments, output should be - # the str returned by ``common.config.Path`` when - # string representation of the path is input. - expected_output = Path()(path_as_str) - - # check output is as expected in all cases - self.assertEqual( - path_standardization_func(path_as_str), - expected_output, - msg=( - "Path-like validator output from str input " - "does not match expected value." - ), - ) - self.assertEqual( - path_standardization_func(path_as_bytes), - expected_output, - msg=( - "Path-like validator output from bytes input " - "does not match expected value." - ), - ) - self.assertEqual( - path_standardization_func(path_like_from_str), - expected_output, - msg=( - "Path-like validator output from path-like input " - "derived from str does not match expected value." - ), - ) - self.assertEqual( - path_standardization_func(path_like_from_bytes), - expected_output, - msg=( - "Path-like validator output from path-like input " - "derived from bytes does not match expected value." - ), - ) - - class TestPositiveIntOrMinusOne(unittest.TestCase): """ Test validator for -1 or positive int works as expected. diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 179f986fdac..028a9f38da1 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -283,48 +283,6 @@ def generate_shape_str(shape, required_shape): ) -class UncertaintySetDomain: - """ - Domain validator for uncertainty set argument. - """ - - def __call__(self, obj): - """ - Type validate uncertainty set object. - - Parameters - ---------- - obj : object - Object to validate. - - Returns - ------- - obj : object - Object that was passed, provided type validation successful. - - Raises - ------ - ValueError - If type validation failed. - """ - if not isinstance(obj, UncertaintySet): - raise ValueError( - f"Expected an {UncertaintySet.__name__} object, " - f"instead received object {obj}" - ) - return obj - - def domain_name(self): - """ - Domain name of self. - """ - return UncertaintySet.__name__ - - -# maintain compatibility with prior versions -uncertainty_sets = UncertaintySetDomain() - - def column(matrix, i): # Get column i of a given multi-dimensional list return [row[i] for row in matrix] From d8b0ba354cb14d3b538dbcdbc41860b694d3afba Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 16 Feb 2024 15:42:23 -0700 Subject: [PATCH 0358/1178] bug --- pyomo/contrib/solver/gurobi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index c1b02c08ef9..c55565e20fb 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -239,7 +239,7 @@ class Gurobi(PersistentSolverUtils, PersistentSolverBase): def __init__(self, **kwds): PersistentSolverUtils.__init__(self) PersistentSolverBase.__init__(self, **kwds) - self._num_instances += 1 + Gurobi._num_instances += 1 self._solver_model = None self._symbol_map = SymbolMap() self._labeler = None @@ -310,8 +310,8 @@ def release_license(self): def __del__(self): if not python_is_shutting_down(): - self._num_instances -= 1 - if self._num_instances == 0: + Gurobi._num_instances -= 1 + if Gurobi._num_instances == 0: self.release_license() def version(self): From 350c3c37fa61691f250199ed8574bacd34fb569c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:19:21 -0700 Subject: [PATCH 0359/1178] Working rewrite of hull that handles nested correctly, many changes to tests because of this --- pyomo/gdp/plugins/hull.py | 386 ++++++++++++++++---------------- pyomo/gdp/tests/common_tests.py | 69 ++++-- pyomo/gdp/tests/models.py | 2 +- pyomo/gdp/tests/test_hull.py | 212 ++++++++++-------- 4 files changed, 366 insertions(+), 303 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index c7a005bb4ea..e880d599366 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -50,6 +50,7 @@ _warn_for_active_disjunct, ) from pyomo.core.util import target_list +from pyomo.util.vars_from_expressions import get_vars_from_components from weakref import ref as weakref_ref logger = logging.getLogger('pyomo.gdp.hull') @@ -224,10 +225,11 @@ def _get_user_defined_local_vars(self, targets): if t.ctype is Disjunct or isinstance(t, _DisjunctData): # first look beneath where we are (there could be Blocks on this # disjunct) - for b in t.component_data_objects(Block, descend_into=Block, - active=True, - sort=SortComponents.deterministic - ): + for b in t.component_data_objects( + Block, descend_into=Block, + active=True, + sort=SortComponents.deterministic + ): if b not in seen_blocks: self._collect_local_vars_from_block(b, user_defined_local_vars) seen_blocks.add(b) @@ -282,6 +284,7 @@ def _apply_to_impl(self, instance, **kwds): # nested GDPs, we will introduce variables that need disaggregating into # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() + # Get all LocalVars from Suffixes ahead of time local_vars_by_disjunct = self._get_user_defined_local_vars( preprocessed_targets) @@ -303,15 +306,7 @@ def _add_transformation_block(self, to_block): return transBlock, new_block transBlock.lbub = Set(initialize=['lb', 'ub', 'eq']) - # Map between disaggregated variables and their - # originals - transBlock._disaggregatedVarMap = { - 'srcVar': ComponentMap(), - 'disaggregatedVar': ComponentMap(), - } - # Map between disaggregated variables and their lb*indicator <= var <= - # ub*indicator constraints - transBlock._bigMConstraintMap = ComponentMap() + # We will store all of the disaggregation constraints for any # Disjunctions we transform onto this block here. transBlock.disaggregationConstraints = Constraint(NonNegativeIntegers) @@ -329,6 +324,7 @@ def _add_transformation_block(self, to_block): def _transform_disjunctionData(self, obj, index, parent_disjunct, local_vars_by_disjunct): + print("Transforming Disjunction %s" % obj) # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -338,8 +334,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, "Must be an XOR!" % obj.name ) # collect the Disjuncts we are going to transform now because we will - # change their active status when we transform them, but still need this - # list after the fact. + # change their active status when we transform them, but we still need + # this list after the fact. active_disjuncts = [disj for disj in obj.disjuncts if disj.active] # We put *all* transformed things on the parent Block of this @@ -357,19 +353,23 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # We first go through and collect all the variables that we are going to # disaggregate. We do this in its own pass because we want to know all - # the Disjuncts that each Var appears in. + # the Disjuncts that each Var appears in since that will tell us exactly + # which diaggregated variables we need. var_order = ComponentSet() disjuncts_var_appears_in = ComponentMap() + # For each disjunct in the disjunction, we will store a list of Vars + # that need a disaggregated counterpart in that disjunct. + disjunct_disaggregated_var_map = {} for disjunct in active_disjuncts: # create the key for each disjunct now - transBlock._disaggregatedVarMap['disaggregatedVar'][ - disjunct - ] = ComponentMap() - for cons in disjunct.component_data_objects( - Constraint, - active=True, - sort=SortComponents.deterministic, - descend_into=Block, + disjunct_disaggregated_var_map[disjunct] = ComponentMap() + for var in get_vars_from_components( + disjunct, + Constraint, + include_fixed=not self._config.assume_fixed_vars_permanent, + active=True, + sort=SortComponents.deterministic, + descend_into=Block ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -378,21 +378,20 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them - for var in EXPR.identify_variables( - cons.body, include_fixed=not - self._config.assume_fixed_vars_permanent): - # Note that, because ComponentSets are ordered, we will - # eventually disaggregate the vars in a deterministic order - # (the order that we found them) - if var not in var_order: - var_order.add(var) - disjuncts_var_appears_in[var] = ComponentSet([disjunct]) - else: - disjuncts_var_appears_in[var].add(disjunct) + + # Note that, because ComponentSets are ordered, we will + # eventually disaggregate the vars in a deterministic order + # (the order that we found them) + if var not in var_order: + var_order.add(var) + disjuncts_var_appears_in[var] = ComponentSet([disjunct]) + else: + disjuncts_var_appears_in[var].add(disjunct) - # We will disaggregate all variables that are not explicitly declared as - # being local. We have marked our own disaggregated variables as local, - # so they will not be re-disaggregated. + # Now, we will disaggregate all variables that are not explicitly + # declared as being local. If we are moving up in a nested tree, we have + # marked our own disaggregated variables as local, so they will not be + # re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} all_vars_to_disaggregate = ComponentSet() # We will ignore variables declared as local in a Disjunct that don't @@ -407,27 +406,22 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, if self._generate_debug_messages: logger.debug( "Assuming '%s' is not a local var since it is" - "used in multiple disjuncts." - % var.getname(fully_qualified=True) + "used in multiple disjuncts." % var.name ) for disj in disjuncts: vars_to_disaggregate[disj].add(var) all_vars_to_disaggregate.add(var) - else: # disjuncts is a set of length 1 + else: # var only appears in one disjunct disjunct = next(iter(disjuncts)) + # We check if the user declared it as local if disjunct in local_vars_by_disjunct: if var in local_vars_by_disjunct[disjunct]: local_vars[disjunct].add(var) - else: - # It's not declared local to this Disjunct, so we - # disaggregate - vars_to_disaggregate[disjunct].add(var) - all_vars_to_disaggregate.add(var) - else: - # The user didn't declare any local vars for this - # Disjunct, so we know we're disaggregating it - vars_to_disaggregate[disjunct].add(var) - all_vars_to_disaggregate.add(var) + continue + # It's not declared local to this Disjunct, so we + # disaggregate + vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. @@ -438,22 +432,22 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() - if obj.active: + if disjunct.active: self._transform_disjunct( - disjunct, - transBlock, - vars_to_disaggregate[disjunct], - local_vars[disjunct], - parent_local_var_list, - local_vars_by_disjunct[parent_disjunct] + obj=disjunct, + transBlock=transBlock, + vars_to_disaggregate=vars_to_disaggregate[disjunct], + local_vars=local_vars[disjunct], + parent_local_var_suffix=parent_local_var_list, + parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], + disjunct_disaggregated_var_map=disjunct_disaggregated_var_map ) xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) - # add the reaggregation constraints - i = 0 + # Now add the reaggregation constraints for var in all_vars_to_disaggregate: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's @@ -465,8 +459,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # create one more disaggregated var idx = len(disaggregatedVars) disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction + print("Creating extra disaggregated var: '%s'" % disaggregated_var) + # mark this as local because we won't re-disaggregate it if this + # is a nested disjunction if parent_local_var_list is not None: parent_local_var_list.append(disaggregated_var) local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) @@ -475,14 +470,34 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, for disj in disjuncts_var_appears_in[var] ) self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, + original_var=var, + disaggregatedVar=disaggregated_var, + disjunct=obj, + bigmConstraint=disaggregated_var_bounds, + lb_idx=(idx, 'lb'), + ub_idx=(idx, 'ub'), + var_free_indicator=var_free + ) + # Update mappings: + var_info = var.parent_block().private_data() + if 'disaggregated_var_map' not in var_info: + var_info['disaggregated_var_map'] = ComponentMap() + disaggregated_var_map = var_info['disaggregated_var_map'] + dis_var_info = disaggregated_var.parent_block().private_data() + if 'original_var_map' not in dis_var_info: + dis_var_info['original_var_map'] = ComponentMap() + original_var_map = dis_var_info['original_var_map'] + if 'bigm_constraint_map' not in dis_var_info: + dis_var_info['bigm_constraint_map'] = ComponentMap() + bigm_constraint_map = dis_var_info['bigm_constraint_map'] + + if disaggregated_var not in bigm_constraint_map: + bigm_constraint_map[disaggregated_var] = {} + bigm_constraint_map[disaggregated_var][obj] = ( + Reference(disaggregated_var_bounds[idx, :]) ) + original_var_map[disaggregated_var] = var + # For every Disjunct the Var does not appear in, we want to map # that this new variable is its disaggreggated variable. for disj in active_disjuncts: @@ -493,38 +508,28 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disj._transformation_block is not None and disj not in disjuncts_var_appears_in[var] ): - relaxationBlock = disj._transformation_block().parent_block() - relaxationBlock._bigMConstraintMap[disaggregated_var] = ( - Reference(disaggregated_var_bounds[idx, :]) - ) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap[ - 'disaggregatedVar'][disj][ - var - ] = disaggregated_var + if not disj in disaggregated_var_map: + disaggregated_var_map[disj] = ComponentMap() + disaggregated_var_map[disj][var] = disaggregated_var + # start the expression for the reaggregation constraint with + # this var disaggregatedExpr = disaggregated_var else: disaggregatedExpr = 0 for disjunct in disjuncts_var_appears_in[var]: - # We know this Disjunct was active, so it has been transformed now. - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar + disaggregatedExpr += disjunct_disaggregated_var_map[disjunct][var] cons_idx = len(disaggregationConstraint) # We always aggregate to the original var. If this is nested, this - # constraint will be transformed again. + # constraint will be transformed again. (And if it turns out + # everything in it is local, then that transformation won't actually + # change the mathematical expression, so it's okay. disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: + if var in disaggregationConstraintMap: disaggregationConstraintMap[var][obj] = disaggregationConstraint[ cons_idx ] @@ -532,14 +537,13 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, thismap = disaggregationConstraintMap[var] = ComponentMap() thismap[obj] = disaggregationConstraint[cons_idx] - i += 1 - # deactivate for the writers obj.deactivate() def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, - parent_local_var_suffix, parent_disjunct_local_vars): - print("\nTransforming '%s'" % obj.name) + parent_local_var_suffix, parent_disjunct_local_vars, + disjunct_disaggregated_var_map): + print("\nTransforming Disjunct '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -565,7 +569,7 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, if parent_local_var_suffix is not None: parent_local_var_suffix.append(disaggregatedVar) # Record that it's local for our own bookkeeping in case we're in a - # nested situation in *this* transformation + # nested tree in *this* transformation parent_disjunct_local_vars.add(disaggregatedVar) # add the bigm constraint @@ -574,17 +578,26 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, disaggregatedVarName + "_bounds", bigmConstraint ) - print("Adding bounds constraints for '%s'" % var) + print("Adding bounds constraints for '%s', the disaggregated var " + "corresponding to Var '%s' on Disjunct '%s'" % + (disaggregatedVar, var, obj)) self._declare_disaggregated_var_bounds( - var, - disaggregatedVar, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - transBlock, + original_var=var, + disaggregatedVar=disaggregatedVar, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) + # update the bigm constraint mappings + data_dict = disaggregatedVar.parent_block().private_data() + if 'bigm_constraint_map' not in data_dict: + data_dict['bigm_constraint_map'] = ComponentMap() + if disaggregatedVar not in data_dict['bigm_constraint_map']: + data_dict['bigm_constraint_map'][disaggregatedVar] = {} + data_dict['bigm_constraint_map'][disaggregatedVar][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = disaggregatedVar for var in local_vars: # we don't need to disaggregate, i.e., we can use this Var, but we @@ -600,35 +613,30 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, relaxationBlock.add_component(conName, bigmConstraint) parent_block = var.parent_block() - disaggregated_var_map = self._get_disaggregated_var_map(parent_block) print("Adding bounds constraints for local var '%s'" % var) - # TODO: This gets mapped in a place where we can't find it if we ask - # for it from the local var itself. self._declare_disaggregated_var_bounds( - var, - var, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - disaggregated_var_map, + original_var=var, + disaggregatedVar=var, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) - - var_substitute_map = dict( - (id(v), newV) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() - ) - zero_substitute_map = dict( - (id(v), ZeroConstant) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() - ) - zero_substitute_map.update((id(v), ZeroConstant) for v in local_vars) + # update the bigm constraint mappings + data_dict = var.parent_block().private_data() + if 'bigm_constraint_map' not in data_dict: + data_dict['bigm_constraint_map'] = ComponentMap() + if var not in data_dict['bigm_constraint_map']: + data_dict['bigm_constraint_map'][var] = {} + data_dict['bigm_constraint_map'][var][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = var + + var_substitute_map = dict((id(v), newV) for v, newV in + disjunct_disaggregated_var_map[obj].items() ) + zero_substitute_map = dict((id(v), ZeroConstant) for v, newV in + disjunct_disaggregated_var_map[obj].items() ) # Transform each component within this disjunct self._transform_block_components( @@ -650,7 +658,6 @@ def _declare_disaggregated_var_bounds( lb_idx, ub_idx, var_free_indicator, - disaggregated_var_map, ): lb = original_var.lb ub = original_var.ub @@ -669,19 +676,24 @@ def _declare_disaggregated_var_bounds( if ub: bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) + original_var_info = original_var.parent_block().private_data() + if 'disaggregated_var_map' not in original_var_info: + original_var_info['disaggregated_var_map'] = ComponentMap() + disaggregated_var_map = original_var_info['disaggregated_var_map'] + + disaggregated_var_info = disaggregatedVar.parent_block().private_data() + if 'original_var_map' not in disaggregated_var_info: + disaggregated_var_info['original_var_map'] = ComponentMap() + original_var_map = disaggregated_var_info['original_var_map'] + # store the mappings from variables to their disaggregated selves on # the transformation block - disaggregated_var_map['disaggregatedVar'][disjunct][ - original_var] = disaggregatedVar - disaggregated_var_map['srcVar'][disaggregatedVar] = original_var - bigMConstraintMap[disaggregatedVar] = bigmConstraint - - # if transBlock is not None: - # transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ - # original_var - # ] = disaggregatedVar - # transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var - # transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint + if disjunct not in disaggregated_var_map: + disaggregated_var_map[disjunct] = ComponentMap() + print("DISAGGREGATED VAR MAP (%s, %s) : %s" % (disjunct, original_var, + disaggregatedVar)) + disaggregated_var_map[disjunct][original_var] = disaggregatedVar + original_var_map[disaggregatedVar] = original_var def _get_local_var_list(self, parent_disjunct): # Add or retrieve Suffix from parent_disjunct so that, if this is @@ -903,17 +915,19 @@ def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ if disjunct._transformation_block is None: raise GDP_Error("Disjunct '%s' has not been transformed" % disjunct.name) - transBlock = disjunct._transformation_block().parent_block() - try: - return transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][v] - except: - if raise_exception: - logger.error( - "It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name) - ) - raise - return none + msg = ("It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name)) + var_map = v.parent_block().private_data() + if 'disaggregated_var_map' in var_map: + try: + return var_map['disaggregated_var_map'][disjunct][v] + except: + if raise_exception: + logger.error(msg) + raise + elif raise_exception: + raise GDP_Error(msg) + return None def get_src_var(self, disaggregated_var): """ @@ -927,23 +941,13 @@ def get_src_var(self, disaggregated_var): (and so appears on a transformation block of some Disjunct) """ - msg = ( + var_map = disaggregated_var.parent_block().private_data() + if 'original_var_map' in var_map: + if disaggregated_var in var_map['original_var_map']: + return var_map['original_var_map'][disaggregated_var] + raise GDP_Error( "'%s' does not appear to be a " - "disaggregated variable" % disaggregated_var.name - ) - # We always put a dictionary called '_disaggregatedVarMap' on the parent - # block of the variable. If it's not there, then this probably isn't a - # disaggregated Var (or if it is it's a developer error). Similarly, if - # the var isn't in the dictionary, if we're doing what we should, then - # it's not a disaggregated var. - transBlock = disaggregated_var.parent_block() - if not hasattr(transBlock, '_disaggregatedVarMap'): - raise GDP_Error(msg) - try: - return transBlock._disaggregatedVarMap['srcVar'][disaggregated_var] - except: - logger.error(msg) - raise + "disaggregated variable" % disaggregated_var.name) # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction @@ -990,7 +994,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, cons = self.get_transformed_constraints(cons)[0] return cons - def get_var_bounds_constraint(self, v): + def get_var_bounds_constraint(self, v, disjunct=None): """ Returns the IndexedConstraint which sets a disaggregated variable to be within its bounds when its Disjunct is active and to @@ -1002,36 +1006,32 @@ def get_var_bounds_constraint(self, v): v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) + disjunct: (For nested Disjunctions) Which Disjunct in the + hierarchy the bounds Constraint should correspond to. + Optional since for non-nested models this can be inferred. """ - msg = ( + info = v.parent_block().private_data() + if 'bigm_constraint_map' in info: + if v in info['bigm_constraint_map']: + if len(info['bigm_constraint_map'][v]) == 1: + # Not nested, or it's at the top layer, so we're fine. + return list(info['bigm_constraint_map'][v].values())[0] + elif disjunct is not None: + # This is nested, so we need to walk up to find the active ones + return info['bigm_constraint_map'][v][disjunct] + else: + raise ValueError( + "It appears that the variable '%s' appears " + "within a nested GDP hierarchy, and no " + "'disjunct' argument was specified. Please " + "specify for which Disjunct the bounds " + "constraint for '%s' should be returned." + % (v, v)) + raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " "been properly transformed." % v.name ) - # This can only go well if v is a disaggregated var - transBlock = v.parent_block() - if not hasattr(transBlock, '_bigMConstraintMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise - try: - cons = transBlock._bigMConstraintMap[v] - except: - logger.error(msg) - raise - transformed_cons = {key: con for key, con in cons.items()} - def is_active(cons): - return all(c.active for c in cons.values()) - while not is_active(transformed_cons): - if 'lb' in transformed_cons: - transformed_cons['lb'] = self.get_transformed_constraints( - transformed_cons['lb'])[0] - if 'ub' in transformed_cons: - transformed_cons['ub'] = self.get_transformed_constraints( - transformed_cons['ub'])[0] - return transformed_cons def get_transformed_constraints(self, cons): cons = super().get_transformed_constraints(cons) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index c5e750e4f08..bef05a78cf6 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -697,32 +697,29 @@ def check_indexedDisj_only_targets_transformed(self, transformation): trans.get_transformed_constraints(m.disjunct1[1, 0].c)[0] .parent_block() .parent_block(), - disjBlock[2], + disjBlock[0], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[1, 1].c)[0].parent_block(), - disjBlock[3], + disjBlock[1], ) # In the disaggregated var bounds self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 0].c)[0] .parent_block() .parent_block(), - disjBlock[0], + disjBlock[2], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 1].c)[0].parent_block(), - disjBlock[1], + disjBlock[3], ) # This relies on the disjunctions being transformed in the same order # every time. These are the mappings between the indices of the original # disjuncts and the indices on the indexed block on the transformation # block. - if transformation == 'bigm': - pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] - elif transformation == 'hull': - pairs = [((2, 0), 0), ((2, 1), 1), ((1, 0), 2), ((1, 1), 3)] + pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] for i, j in pairs: self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.disjunct1[i]) @@ -1731,35 +1728,69 @@ def check_transformation_blocks_nestedDisjunctions(self, m, transformation): # This is a much more comprehensive test that doesn't depend on # transformation Block structure, so just reuse it: hull = TransformationFactory('gdp.hull') - d3 = hull.get_disaggregated_var(m.d1.d3.indicator_var, m.d1) - d4 = hull.get_disaggregated_var(m.d1.d4.indicator_var, m.d1) + d3 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d1) + d4 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d1) self.check_transformed_model_nestedDisjuncts(m, d3, d4) - # check the disaggregated indicator var bound constraints too - cons = hull.get_var_bounds_constraint(d3) + # Check the 4 constraints that are unique to the case where we didn't + # declare d1.d3 and d1.d4 as local + d32 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d2) + d42 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d2) + # check the additional disaggregated indicator var bound constraints + cons = hull.get_var_bounds_constraint(d32) self.assertEqual(len(cons), 1) check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d32 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 assertExpressionsEqual( self, cons_expr, - d3 - m.d1.binary_indicator_var <= 0.0 + d32 + m.d1.binary_indicator_var - 1 <= 0.0 ) - cons = hull.get_var_bounds_constraint(d4) + cons = hull.get_var_bounds_constraint(d42) self.assertEqual(len(cons), 1) check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d42 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 + assertExpressionsEqual( + self, + cons_expr, + d42 + m.d1.binary_indicator_var - 1 <= 0.0 + ) + # check the aggregation constraints for the disaggregated indicator vars + cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, + m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 + ) + cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, + m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) assertExpressionsEqual( self, cons_expr, - d4 - m.d1.binary_indicator_var <= 0.0 + m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 ) - num_cons = len(m.component_data_objects(Constraint, - active=True, - descend_into=Block)) - self.assertEqual(num_cons, 10) + num_cons = len(list(m.component_data_objects(Constraint, + active=True, + descend_into=Block))) + # 30 total constraints in transformed model minus 10 trivial bounds + # (lower bounds of 0) gives us 20 constraints total: + self.assertEqual(num_cons, 20) + # (And this is 4 more than we test in + # self.check_transformed_model_nestedDisjuncts, so that's comforting + # too.) def check_nested_disjunction_target(self, transformation): diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index a52f08b790e..3477d182241 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -463,7 +463,7 @@ def makeNestedDisjunctions(): (makeNestedDisjunctions_NestedDisjuncts is a much simpler model. All this adds is that it has a nested disjunction on a DisjunctData as well - as on a SimpleDisjunct. So mostly it exists for historical reasons.) + as on a ScalarDisjunct. So mostly it exists for historical reasons.) """ m = ConcreteModel() m.x = Var(bounds=(-9, 9)) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 436367b3a89..b3bfbaaf8da 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -412,13 +412,11 @@ def test_error_for_or(self): ) def check_disaggregation_constraint(self, cons, var, disvar1, disvar2): - repn = generate_standard_repn(cons.body) - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - self.assertEqual(len(repn.linear_vars), 3) - ct.check_linear_coef(self, repn, var, 1) - ct.check_linear_coef(self, repn, disvar1, -1) - ct.check_linear_coef(self, repn, disvar2, -1) + assertExpressionsEqual( + self, + cons.expr, + var == disvar1 + disvar2 + ) def test_disaggregation_constraint(self): m = models.makeTwoTermDisj_Nonlinear() @@ -430,8 +428,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.w, m.disjunction), m.w, - disjBlock[1].disaggregatedVars.w, transBlock._disaggregatedVars[1], + disjBlock[1].disaggregatedVars.w, ) self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.x, m.disjunction), @@ -442,8 +440,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.y, m.disjunction), m.y, - disjBlock[0].disaggregatedVars.y, transBlock._disaggregatedVars[0], + disjBlock[0].disaggregatedVars.y, ) def test_xor_constraint_mapping(self): @@ -672,17 +670,38 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): self.assertIs(hull.get_src_var(x), m.disj1.x) # there is a spare x on disjunction1's block - x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[2] + x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[0] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj2), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) + # What really matters is that the above matches this: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction1) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1) + ) # and both a spare x and y on disjunction2's block - x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[0] - y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[2] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj4), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) self.assertIs(hull.get_disaggregated_var(m.disj1.y, m.disj3), y1) self.assertIs(hull.get_src_var(y1), m.disj1.y) + # and again what really matters is that these align with the + # disaggregation constraints: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3) + ) + agg_cons = hull.get_disaggregation_constraint(m.disj1.y, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4) + ) def check_name_collision_disaggregated_vars(self, m, disj): hull = TransformationFactory('gdp.hull') @@ -1105,7 +1124,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertEqual(len(transBlock1.relaxedDisjuncts), 4) hull = TransformationFactory('gdp.hull') - firstTerm2 = transBlock1.relaxedDisjuncts[0] + firstTerm2 = transBlock1.relaxedDisjuncts[2] self.assertIs(firstTerm2, m.firstTerm[2].transformation_block) self.assertIsInstance(firstTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.firstTerm[2].cons) @@ -1119,7 +1138,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm2) self.assertEqual(len(cons), 2) - secondTerm2 = transBlock1.relaxedDisjuncts[1] + secondTerm2 = transBlock1.relaxedDisjuncts[3] self.assertIs(secondTerm2, m.secondTerm[2].transformation_block) self.assertIsInstance(secondTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[2].cons) @@ -1133,7 +1152,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), secondTerm2) self.assertEqual(len(cons), 2) - firstTerm1 = transBlock1.relaxedDisjuncts[2] + firstTerm1 = transBlock1.relaxedDisjuncts[0] self.assertIs(firstTerm1, m.firstTerm[1].transformation_block) self.assertIsInstance(firstTerm1.disaggregatedVars.component("x"), Var) self.assertTrue(firstTerm1.disaggregatedVars.x.is_fixed()) @@ -1151,7 +1170,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm1) self.assertEqual(len(cons), 2) - secondTerm1 = transBlock1.relaxedDisjuncts[3] + secondTerm1 = transBlock1.relaxedDisjuncts[1] self.assertIs(secondTerm1, m.secondTerm[1].transformation_block) self.assertIsInstance(secondTerm1.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[1].cons) @@ -1379,9 +1398,8 @@ def test_deactivated_disjunct_leaves_nested_disjuncts_active(self): ct.check_deactivated_disjunct_leaves_nested_disjunct_active(self, 'hull') def test_mappings_between_disjunctions_and_xors(self): - # This test is nearly identical to the one in bigm, but because of - # different transformation orders, the name conflict gets resolved in - # the opposite way. + # Tests that the XOR constraints are put on the parent block of the + # disjunction, and checks the mappings. m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.hull') transform.apply_to(m) @@ -1390,8 +1408,10 @@ def test_mappings_between_disjunctions_and_xors(self): disjunctionPairs = [ (m.disjunction, transBlock.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock.innerdisjunction_xor[0]), - (m.simpledisjunct.innerdisjunction, transBlock.innerdisjunction_xor_4), + (m.disjunct[1].innerdisjunction[0], + m.disjunct[1].innerdisjunction[0].algebraic_constraint.parent_block().innerdisjunction_xor[0]), + (m.simpledisjunct.innerdisjunction, + m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor), ] # check disjunction mappings @@ -1568,24 +1588,23 @@ def test_transformed_model_nestedDisjuncts(self): m.d1.d4.binary_indicator_var) # Last, check that there aren't things we weren't expecting - all_cons = list(m.component_data_objects(Constraint, active=True, descend_into=Block)) - num_cons = len(all_cons) - - for idx, cons in enumerate(all_cons): - print(idx) - print(cons.name) - print(cons.expr) - print("") # 2 disaggregation constraints for x 0,3 - # + 6 bounds constraints for x 6,8,9,13,14,16 These are dumb: 10,14,16 + # + 6 bounds constraints for x 6,8,9,13,14,16 # + 2 bounds constraints for inner indicator vars 11, 12 # + 2 exactly-one constraints 1,4 # + 4 transformed constraints 2,5,7,15 - self.assertEqual(num_cons, 16) + self.assertEqual(len(all_cons), 16) def check_transformed_model_nestedDisjuncts(self, m, d3, d4): + # This function checks all of the 16 constraint expressions from + # transforming models.makeNestedDisjunction_NestedDisjuncts when + # declaring the inner indicator vars (d3 and d4) as local. Note that it + # also is a correct test for the case where the inner indicator vars are + # *not* declared as local, but not a complete one, since there are + # additional constraints in that case (see + # check_transformation_blocks_nestedDisjunctions in common_tests.py). hull = TransformationFactory('gdp.hull') transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) @@ -1654,7 +1673,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): assertExpressionsEqual( self, cons_expr, - 1.2*m.d1.d3.binary_indicator_var - x_d3 <= 0.0 + 1.2*d3 - x_d3 <= 0.0 ) cons = hull.get_transformed_constraints(m.d1.d4.c) @@ -1665,7 +1684,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): assertExpressionsEqual( self, cons_expr, - 1.3*m.d1.d4.binary_indicator_var - x_d4 <= 0.0 + 1.3*d4 - x_d4 <= 0.0 ) cons = hull.get_transformed_constraints(m.d1.c) @@ -1711,41 +1730,69 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): cons_expr, x_d2 - 2*m.d2.binary_indicator_var <= 0.0 ) - cons = hull.get_var_bounds_constraint(x_d3) + cons = hull.get_var_bounds_constraint(x_d3, m.d1.d3) # the lb is trivial in this case, so we just have 1 self.assertEqual(len(cons), 1) - ct.check_obj_in_active_tree(self, cons['ub']) - cons_expr = self.simplify_leq_cons(cons['ub']) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( self, cons_expr, x_d3 - 2*d3 <= 0.0 ) - cons = hull.get_var_bounds_constraint(x_d4) + cons = hull.get_var_bounds_constraint(x_d4, m.d1.d4) # the lb is trivial in this case, so we just have 1 self.assertEqual(len(cons), 1) - ct.check_obj_in_active_tree(self, cons['ub']) - cons_expr = self.simplify_leq_cons(cons['ub']) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( self, cons_expr, x_d4 - 2*d4 <= 0.0 ) + cons = hull.get_var_bounds_constraint(x_d3, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, + cons_expr, + x_d3 - 2*m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d4, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, + cons_expr, + x_d4 - 2*m.d1.binary_indicator_var <= 0.0 + ) # Bounds constraints for local vars - cons = hull.get_var_bounds_constraint(m.d1.d3.binary_indicator_var) + cons = hull.get_var_bounds_constraint(d3) ct.check_obj_in_active_tree(self, cons['ub']) assertExpressionsEqual( self, cons['ub'].expr, - m.d1.d3.binary_indicator_var <= m.d1.binary_indicator_var + d3 <= m.d1.binary_indicator_var ) - cons = hull.get_var_bounds_constraint(m.d1.d4.binary_indicator_var) + cons = hull.get_var_bounds_constraint(d4) ct.check_obj_in_active_tree(self, cons['ub']) assertExpressionsEqual( self, cons['ub'].expr, - m.d1.d4.binary_indicator_var <= m.d1.binary_indicator_var + d4 <= m.d1.binary_indicator_var ) @unittest.skipIf(not linear_solvers, "No linear solver available") @@ -1853,6 +1900,9 @@ def d_r(e): e.c1 = Constraint(expr=e.lambdas[1] + e.lambdas[2] == 1) e.c2 = Constraint(expr=m.x == 2 * e.lambdas[1] + 3 * e.lambdas[2]) + d.LocalVars = Suffix(direction=Suffix.LOCAL) + d.LocalVars[d] = [d.d_l.indicator_var.get_associated_binary(), + d.d_r.indicator_var.get_associated_binary()] d.inner_disj = Disjunction(expr=[d.d_l, d.d_r]) m.disj = Disjunction(expr=[m.d_l, m.d_r]) @@ -1875,28 +1925,29 @@ def d_r(e): cons = hull.get_transformed_constraints(d.c1) self.assertEqual(len(cons), 1) convex_combo = cons[0] + convex_combo_expr = self.simplify_cons(convex_combo) assertExpressionsEqual( self, - convex_combo.expr, - lambda1 + lambda2 - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == d.indicator_var.get_associated_binary(), + convex_combo_expr, + lambda1 + lambda2 - d.indicator_var.get_associated_binary() + == 0.0, ) cons = hull.get_transformed_constraints(d.c2) self.assertEqual(len(cons), 1) get_x = cons[0] + get_x_expr = self.simplify_cons(get_x) assertExpressionsEqual( self, - get_x.expr, - x - - (2 * lambda1 + 3 * lambda2) - - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == 0.0 * d.indicator_var.get_associated_binary(), + get_x_expr, + x - 2 * lambda1 - 3 * lambda2 + == 0.0, ) cons = hull.get_disaggregation_constraint(m.x, m.disj) assertExpressionsEqual(self, cons.expr, m.x == x1 + x2) cons = hull.get_disaggregation_constraint(m.x, m.d_r.inner_disj) - assertExpressionsEqual(self, cons.expr, x2 == x3 + x4) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x2 - x3 - x4 == 0.0) def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): m = ConcreteModel() @@ -1949,7 +2000,7 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): x_c3 == 0.0) def simplify_cons(self, cons): - visitor = LinearRepnVisitor({}, {}, {}) + visitor = LinearRepnVisitor({}, {}, {}, None) lb = cons.lower ub = cons.upper self.assertEqual(cons.lb, cons.ub) @@ -1958,7 +2009,7 @@ def simplify_cons(self, cons): return repn.to_expression(visitor) == lb def simplify_leq_cons(self, cons): - visitor = LinearRepnVisitor({}, {}, {}) + visitor = LinearRepnVisitor({}, {}, {}, None) self.assertIsNone(cons.lower) ub = cons.upper repn = visitor.walk_expression(cons.body) @@ -2294,20 +2345,13 @@ def test_mapping_method_errors(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_var_bounds_constraint, - m.w, - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, ".*Either 'w' is not a disaggregated variable, " "or the disjunction that disaggregates it has " "not been properly transformed.", - ) + ): + hull.get_var_bounds_constraint(m.w) log = StringIO() with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): @@ -2328,36 +2372,24 @@ def test_mapping_method_errors(self): r"Disjunction 'disjunction'", ) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_src_var, - m.w, - ) - self.assertRegex( - log.getvalue(), ".*'w' does not appear to be a disaggregated variable" - ) + with self.assertRaisesRegex( + GDP_Error, + ".*'w' does not appear to be a disaggregated variable" + ): + hull.get_src_var(m.w,) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", - hull.get_disaggregated_var, - m.d[1].transformation_block.disaggregatedVars.w, - m.d[1], - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, r".*It does not appear " r"'_pyomo_gdp_hull_reformulation." r"relaxedDisjuncts\[1\].disaggregatedVars.w' " r"is a variable that appears in disjunct " - r"'d\[1\]'", - ) + r"'d\[1\]'" + ): + hull.get_disaggregated_var( + m.d[1].transformation_block.disaggregatedVars.w, + m.d[1], + ) m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7]) self.assertRaisesRegex( From 0ff74b63eed54196534b04ee57c607c041aa5b3f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:21:04 -0700 Subject: [PATCH 0360/1178] Black has opinions --- pyomo/gdp/plugins/hull.py | 116 +++++++++++++---------- pyomo/gdp/tests/common_tests.py | 28 ++---- pyomo/gdp/tests/test_hull.py | 163 ++++++++++++-------------------- 3 files changed, 133 insertions(+), 174 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index e880d599366..9fcac6a8f4e 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -57,6 +57,7 @@ from pytest import set_trace + @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." ) @@ -226,9 +227,10 @@ def _get_user_defined_local_vars(self, targets): # first look beneath where we are (there could be Blocks on this # disjunct) for b in t.component_data_objects( - Block, descend_into=Block, - active=True, - sort=SortComponents.deterministic + Block, + descend_into=Block, + active=True, + sort=SortComponents.deterministic, ): if b not in seen_blocks: self._collect_local_vars_from_block(b, user_defined_local_vars) @@ -237,8 +239,9 @@ def _get_user_defined_local_vars(self, targets): blk = t while blk is not None: if blk not in seen_blocks: - self._collect_local_vars_from_block(blk, - user_defined_local_vars) + self._collect_local_vars_from_block( + blk, user_defined_local_vars + ) seen_blocks.add(blk) blk = blk.parent_block() return user_defined_local_vars @@ -285,16 +288,12 @@ def _apply_to_impl(self, instance, **kwds): # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() # Get all LocalVars from Suffixes ahead of time - local_vars_by_disjunct = self._get_user_defined_local_vars( - preprocessed_targets) + local_vars_by_disjunct = self._get_user_defined_local_vars(preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: self._transform_disjunctionData( - t, - t.index(), - gdp_tree.parent(t), - local_vars_by_disjunct + t, t.index(), gdp_tree.parent(t), local_vars_by_disjunct ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -322,8 +321,9 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData(self, obj, index, parent_disjunct, - local_vars_by_disjunct): + def _transform_disjunctionData( + self, obj, index, parent_disjunct, local_vars_by_disjunct + ): print("Transforming Disjunction %s" % obj) # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up @@ -364,12 +364,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # create the key for each disjunct now disjunct_disaggregated_var_map[disjunct] = ComponentMap() for var in get_vars_from_components( - disjunct, - Constraint, - include_fixed=not self._config.assume_fixed_vars_permanent, - active=True, - sort=SortComponents.deterministic, - descend_into=Block + disjunct, + Constraint, + include_fixed=not self._config.assume_fixed_vars_permanent, + active=True, + sort=SortComponents.deterministic, + descend_into=Block, ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -378,7 +378,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them - + # Note that, because ComponentSets are ordered, we will # eventually disaggregate the vars in a deterministic order # (the order that we found them) @@ -411,7 +411,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, for disj in disjuncts: vars_to_disaggregate[disj].add(var) all_vars_to_disaggregate.add(var) - else: # var only appears in one disjunct + else: # var only appears in one disjunct disjunct = next(iter(disjuncts)) # We check if the user declared it as local if disjunct in local_vars_by_disjunct: @@ -440,7 +440,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, local_vars=local_vars[disjunct], parent_local_var_suffix=parent_local_var_list, parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], - disjunct_disaggregated_var_map=disjunct_disaggregated_var_map + disjunct_disaggregated_var_map=disjunct_disaggregated_var_map, ) xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as @@ -476,7 +476,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, bigmConstraint=disaggregated_var_bounds, lb_idx=(idx, 'lb'), ub_idx=(idx, 'ub'), - var_free_indicator=var_free + var_free_indicator=var_free, ) # Update mappings: var_info = var.parent_block().private_data() @@ -493,8 +493,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, if disaggregated_var not in bigm_constraint_map: bigm_constraint_map[disaggregated_var] = {} - bigm_constraint_map[disaggregated_var][obj] = ( - Reference(disaggregated_var_bounds[idx, :]) + bigm_constraint_map[disaggregated_var][obj] = Reference( + disaggregated_var_bounds[idx, :] ) original_var_map[disaggregated_var] = var @@ -540,9 +540,16 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, - parent_local_var_suffix, parent_disjunct_local_vars, - disjunct_disaggregated_var_map): + def _transform_disjunct( + self, + obj, + transBlock, + vars_to_disaggregate, + local_vars, + parent_local_var_suffix, + parent_disjunct_local_vars, + disjunct_disaggregated_var_map, + ): print("\nTransforming Disjunct '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) @@ -578,9 +585,11 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, disaggregatedVarName + "_bounds", bigmConstraint ) - print("Adding bounds constraints for '%s', the disaggregated var " - "corresponding to Var '%s' on Disjunct '%s'" % - (disaggregatedVar, var, obj)) + print( + "Adding bounds constraints for '%s', the disaggregated var " + "corresponding to Var '%s' on Disjunct '%s'" + % (disaggregatedVar, var, obj) + ) self._declare_disaggregated_var_bounds( original_var=var, disaggregatedVar=disaggregatedVar, @@ -633,10 +642,13 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, data_dict['bigm_constraint_map'][var][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = var - var_substitute_map = dict((id(v), newV) for v, newV in - disjunct_disaggregated_var_map[obj].items() ) - zero_substitute_map = dict((id(v), ZeroConstant) for v, newV in - disjunct_disaggregated_var_map[obj].items() ) + var_substitute_map = dict( + (id(v), newV) for v, newV in disjunct_disaggregated_var_map[obj].items() + ) + zero_substitute_map = dict( + (id(v), ZeroConstant) + for v, newV in disjunct_disaggregated_var_map[obj].items() + ) # Transform each component within this disjunct self._transform_block_components( @@ -690,8 +702,10 @@ def _declare_disaggregated_var_bounds( # the transformation block if disjunct not in disaggregated_var_map: disaggregated_var_map[disjunct] = ComponentMap() - print("DISAGGREGATED VAR MAP (%s, %s) : %s" % (disjunct, original_var, - disaggregatedVar)) + print( + "DISAGGREGATED VAR MAP (%s, %s) : %s" + % (disjunct, original_var, disaggregatedVar) + ) disaggregated_var_map[disjunct][original_var] = disaggregatedVar original_var_map[disaggregatedVar] = original_var @@ -915,8 +929,10 @@ def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ if disjunct._transformation_block is None: raise GDP_Error("Disjunct '%s' has not been transformed" % disjunct.name) - msg = ("It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name)) + msg = ( + "It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name) + ) var_map = v.parent_block().private_data() if 'disaggregated_var_map' in var_map: try: @@ -947,12 +963,14 @@ def get_src_var(self, disaggregated_var): return var_map['original_var_map'][disaggregated_var] raise GDP_Error( "'%s' does not appear to be a " - "disaggregated variable" % disaggregated_var.name) + "disaggregated variable" % disaggregated_var.name + ) # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction - def get_disaggregation_constraint(self, original_var, disjunction, - raise_exception=True): + def get_disaggregation_constraint( + self, original_var, disjunction, raise_exception=True + ): """ Returns the disaggregation (re-aggregation?) constraint (which links the disaggregated variables to their original) @@ -976,11 +994,9 @@ def get_disaggregation_constraint(self, original_var, disjunction, ) try: - cons = ( - transBlock - .parent_block() - ._disaggregationConstraintMap[original_var][disjunction] - ) + cons = transBlock.parent_block()._disaggregationConstraintMap[original_var][ + disjunction + ] except: if raise_exception: logger.error( @@ -1006,7 +1022,7 @@ def get_var_bounds_constraint(self, v, disjunct=None): v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) - disjunct: (For nested Disjunctions) Which Disjunct in the + disjunct: (For nested Disjunctions) Which Disjunct in the hierarchy the bounds Constraint should correspond to. Optional since for non-nested models this can be inferred. """ @@ -1025,8 +1041,8 @@ def get_var_bounds_constraint(self, v, disjunct=None): "within a nested GDP hierarchy, and no " "'disjunct' argument was specified. Please " "specify for which Disjunct the bounds " - "constraint for '%s' should be returned." - % (v, v)) + "constraint for '%s' should be returned." % (v, v) + ) raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index bef05a78cf6..e63742bb1a8 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1745,9 +1745,7 @@ def check_transformation_blocks_nestedDisjunctions(self, m, transformation): # "extra" disaggregated var that gets created when it need to be # disaggregated for d1, but it's not used in d2 assertExpressionsEqual( - self, - cons_expr, - d32 + m.d1.binary_indicator_var - 1 <= 0.0 + self, cons_expr, d32 + m.d1.binary_indicator_var - 1 <= 0.0 ) cons = hull.get_var_bounds_constraint(d42) @@ -1758,33 +1756,25 @@ def check_transformation_blocks_nestedDisjunctions(self, m, transformation): # "extra" disaggregated var that gets created when it need to be # disaggregated for d1, but it's not used in d2 assertExpressionsEqual( - self, - cons_expr, - d42 + m.d1.binary_indicator_var - 1 <= 0.0 + self, cons_expr, d42 + m.d1.binary_indicator_var - 1 <= 0.0 ) # check the aggregation constraints for the disaggregated indicator vars - cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, - m.disj) + cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, m.disj) check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) assertExpressionsEqual( - self, - cons_expr, - m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 + self, cons_expr, m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 ) - cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, - m.disj) + cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, m.disj) check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) assertExpressionsEqual( - self, - cons_expr, - m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 + self, cons_expr, m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 ) - num_cons = len(list(m.component_data_objects(Constraint, - active=True, - descend_into=Block))) + num_cons = len( + list(m.component_data_objects(Constraint, active=True, descend_into=Block)) + ) # 30 total constraints in transformed model minus 10 trivial bounds # (lower bounds of 0) gives us 20 constraints total: self.assertEqual(num_cons, 20) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b3bfbaaf8da..aef119c0f1e 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -412,11 +412,7 @@ def test_error_for_or(self): ) def check_disaggregation_constraint(self, cons, var, disvar1, disvar2): - assertExpressionsEqual( - self, - cons.expr, - var == disvar1 + disvar2 - ) + assertExpressionsEqual(self, cons.expr, var == disvar1 + disvar2) def test_disaggregation_constraint(self): m = models.makeTwoTermDisj_Nonlinear() @@ -678,7 +674,7 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): assertExpressionsEqual( self, agg_cons.expr, - m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1) + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1), ) # and both a spare x and y on disjunction2's block @@ -694,13 +690,13 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): assertExpressionsEqual( self, agg_cons.expr, - m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3) + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3), ) agg_cons = hull.get_disaggregation_constraint(m.disj1.y, m.disjunction2) assertExpressionsEqual( self, agg_cons.expr, - m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4) + m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4), ) def check_name_collision_disaggregated_vars(self, m, disj): @@ -1408,10 +1404,17 @@ def test_mappings_between_disjunctions_and_xors(self): disjunctionPairs = [ (m.disjunction, transBlock.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], - m.disjunct[1].innerdisjunction[0].algebraic_constraint.parent_block().innerdisjunction_xor[0]), - (m.simpledisjunct.innerdisjunction, - m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor), + ( + m.disjunct[1].innerdisjunction[0], + m.disjunct[1] + .innerdisjunction[0] + .algebraic_constraint.parent_block() + .innerdisjunction_xor[0], + ), + ( + m.simpledisjunct.innerdisjunction, + m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor, + ), ] # check disjunction mappings @@ -1578,18 +1581,20 @@ def test_transformed_model_nestedDisjuncts(self): m.LocalVars[m.d1] = [ m.d1.binary_indicator_var, m.d1.d3.binary_indicator_var, - m.d1.d4.binary_indicator_var + m.d1.d4.binary_indicator_var, ] - + hull = TransformationFactory('gdp.hull') hull.apply_to(m) - self.check_transformed_model_nestedDisjuncts(m, m.d1.d3.binary_indicator_var, - m.d1.d4.binary_indicator_var) + self.check_transformed_model_nestedDisjuncts( + m, m.d1.d3.binary_indicator_var, m.d1.d4.binary_indicator_var + ) # Last, check that there aren't things we weren't expecting - all_cons = list(m.component_data_objects(Constraint, active=True, - descend_into=Block)) + all_cons = list( + m.component_data_objects(Constraint, active=True, descend_into=Block) + ) # 2 disaggregation constraints for x 0,3 # + 6 bounds constraints for x 6,8,9,13,14,16 # + 2 bounds constraints for inner indicator vars 11, 12 @@ -1614,9 +1619,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): self.assertIsInstance(xor, Constraint) ct.check_obj_in_active_tree(self, xor) assertExpressionsEqual( - self, - xor.expr, - m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 + self, xor.expr, m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 ) self.assertIs(xor, m.disj.algebraic_constraint) self.assertIs(m.disj, hull.get_src_disjunction(xor)) @@ -1630,11 +1633,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, xor) xor_expr = self.simplify_cons(xor) assertExpressionsEqual( - self, - xor_expr, - d3 + - d4 - - m.d1.binary_indicator_var == 0.0 + self, xor_expr, d3 + d4 - m.d1.binary_indicator_var == 0.0 ) # check disaggregation constraints @@ -1649,20 +1648,12 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): cons = hull.get_disaggregation_constraint(m.x, m.d1.disj2) ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - x_d1 - x_d3 - x_d4 == 0.0 - ) + assertExpressionsEqual(self, cons_expr, x_d1 - x_d3 - x_d4 == 0.0) # Outer disjunction cons = hull.get_disaggregation_constraint(m.x, m.disj) ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - m.x - x_d1 - x_d2 == 0.0 - ) + assertExpressionsEqual(self, cons_expr, m.x - x_d1 - x_d2 == 0.0) ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) @@ -1670,32 +1661,22 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): cons = cons[0] ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - 1.2*d3 - x_d3 <= 0.0 - ) + assertExpressionsEqual(self, cons_expr, 1.2 * d3 - x_d3 <= 0.0) cons = hull.get_transformed_constraints(m.d1.d4.c) self.assertEqual(len(cons), 1) cons = cons[0] ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - 1.3*d4 - x_d4 <= 0.0 - ) - + assertExpressionsEqual(self, cons_expr, 1.3 * d4 - x_d4 <= 0.0) + cons = hull.get_transformed_constraints(m.d1.c) self.assertEqual(len(cons), 1) cons = cons[0] ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) assertExpressionsEqual( - self, - cons_expr, - 1.0*m.d1.binary_indicator_var - x_d1 <= 0.0 + self, cons_expr, 1.0 * m.d1.binary_indicator_var - x_d1 <= 0.0 ) cons = hull.get_transformed_constraints(m.d2.c) @@ -1704,9 +1685,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) assertExpressionsEqual( - self, - cons_expr, - 1.1*m.d2.binary_indicator_var - x_d2 <= 0.0 + self, cons_expr, 1.1 * m.d2.binary_indicator_var - x_d2 <= 0.0 ) ## Bounds constraints @@ -1716,9 +1695,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) assertExpressionsEqual( - self, - cons_expr, - x_d1 - 2*m.d1.binary_indicator_var <= 0.0 + self, cons_expr, x_d1 - 2 * m.d1.binary_indicator_var <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d2) # the lb is trivial in this case, so we just have 1 @@ -1726,9 +1703,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) assertExpressionsEqual( - self, - cons_expr, - x_d2 - 2*m.d2.binary_indicator_var <= 0.0 + self, cons_expr, x_d2 - 2 * m.d2.binary_indicator_var <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d3, m.d1.d3) # the lb is trivial in this case, so we just have 1 @@ -1739,11 +1714,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ub = cons[0] ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) - assertExpressionsEqual( - self, - cons_expr, - x_d3 - 2*d3 <= 0.0 - ) + assertExpressionsEqual(self, cons_expr, x_d3 - 2 * d3 <= 0.0) cons = hull.get_var_bounds_constraint(x_d4, m.d1.d4) # the lb is trivial in this case, so we just have 1 self.assertEqual(len(cons), 1) @@ -1753,20 +1724,14 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ub = cons[0] ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) - assertExpressionsEqual( - self, - cons_expr, - x_d4 - 2*d4 <= 0.0 - ) + assertExpressionsEqual(self, cons_expr, x_d4 - 2 * d4 <= 0.0) cons = hull.get_var_bounds_constraint(x_d3, m.d1) self.assertEqual(len(cons), 1) ub = cons['ub'] ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( - self, - cons_expr, - x_d3 - 2*m.d1.binary_indicator_var <= 0.0 + self, cons_expr, x_d3 - 2 * m.d1.binary_indicator_var <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d4, m.d1) self.assertEqual(len(cons), 1) @@ -1774,26 +1739,16 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( - self, - cons_expr, - x_d4 - 2*m.d1.binary_indicator_var <= 0.0 + self, cons_expr, x_d4 - 2 * m.d1.binary_indicator_var <= 0.0 ) # Bounds constraints for local vars cons = hull.get_var_bounds_constraint(d3) ct.check_obj_in_active_tree(self, cons['ub']) - assertExpressionsEqual( - self, - cons['ub'].expr, - d3 <= m.d1.binary_indicator_var - ) + assertExpressionsEqual(self, cons['ub'].expr, d3 <= m.d1.binary_indicator_var) cons = hull.get_var_bounds_constraint(d4) ct.check_obj_in_active_tree(self, cons['ub']) - assertExpressionsEqual( - self, - cons['ub'].expr, - d4 <= m.d1.binary_indicator_var - ) + assertExpressionsEqual(self, cons['ub'].expr, d4 <= m.d1.binary_indicator_var) @unittest.skipIf(not linear_solvers, "No linear solver available") def test_solve_nested_model(self): @@ -1804,8 +1759,8 @@ def test_solve_nested_model(self): m.LocalVars[m.d1] = [ m.d1.binary_indicator_var, m.d1.d3.binary_indicator_var, - m.d1.d4.binary_indicator_var - ] + m.d1.d4.binary_indicator_var, + ] hull = TransformationFactory('gdp.hull') m_hull = hull.create_using(m) @@ -1901,8 +1856,10 @@ def d_r(e): e.c2 = Constraint(expr=m.x == 2 * e.lambdas[1] + 3 * e.lambdas[2]) d.LocalVars = Suffix(direction=Suffix.LOCAL) - d.LocalVars[d] = [d.d_l.indicator_var.get_associated_binary(), - d.d_r.indicator_var.get_associated_binary()] + d.LocalVars[d] = [ + d.d_l.indicator_var.get_associated_binary(), + d.d_r.indicator_var.get_associated_binary(), + ] d.inner_disj = Disjunction(expr=[d.d_l, d.d_r]) m.disj = Disjunction(expr=[m.d_l, m.d_r]) @@ -1929,18 +1886,14 @@ def d_r(e): assertExpressionsEqual( self, convex_combo_expr, - lambda1 + lambda2 - d.indicator_var.get_associated_binary() - == 0.0, + lambda1 + lambda2 - d.indicator_var.get_associated_binary() == 0.0, ) cons = hull.get_transformed_constraints(d.c2) self.assertEqual(len(cons), 1) get_x = cons[0] get_x_expr = self.simplify_cons(get_x) assertExpressionsEqual( - self, - get_x_expr, - x - 2 * lambda1 - 3 * lambda2 - == 0.0, + self, get_x_expr, x - 2 * lambda1 - 3 * lambda2 == 0.0 ) cons = hull.get_disaggregation_constraint(m.x, m.disj) @@ -1996,8 +1949,9 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) x_cons_child_expr = self.simplify_cons(x_cons_child) - assertExpressionsEqual(self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - - x_c3 == 0.0) + assertExpressionsEqual( + self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - x_c3 == 0.0 + ) def simplify_cons(self, cons): visitor = LinearRepnVisitor({}, {}, {}, None) @@ -2065,8 +2019,9 @@ def test_nested_with_var_that_skips_a_level(self): self.assertTrue(cons.active) cons_expr = self.simplify_cons(cons) assertExpressionsEqual(self, cons_expr, m.x - x_y1 - x_y2 == 0.0) - cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, - raise_exception=False) + cons = hull.get_disaggregation_constraint( + m.y, m.y1.z1.disjunction, raise_exception=False + ) self.assertIsNone(cons) cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) self.assertTrue(cons.active) @@ -2373,10 +2328,9 @@ def test_mapping_method_errors(self): ) with self.assertRaisesRegex( - GDP_Error, - ".*'w' does not appear to be a disaggregated variable" + GDP_Error, ".*'w' does not appear to be a disaggregated variable" ): - hull.get_src_var(m.w,) + hull.get_src_var(m.w) with self.assertRaisesRegex( GDP_Error, @@ -2384,11 +2338,10 @@ def test_mapping_method_errors(self): r"'_pyomo_gdp_hull_reformulation." r"relaxedDisjuncts\[1\].disaggregatedVars.w' " r"is a variable that appears in disjunct " - r"'d\[1\]'" + r"'d\[1\]'", ): hull.get_disaggregated_var( - m.d[1].transformation_block.disaggregatedVars.w, - m.d[1], + m.d[1].transformation_block.disaggregatedVars.w, m.d[1] ) m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7]) From c9eca76fae1b319b456aa014e7a49b36e213ce8c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:22:21 -0700 Subject: [PATCH 0361/1178] Removing debugging --- pyomo/gdp/plugins/hull.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 9fcac6a8f4e..53dffc7a18e 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -324,7 +324,6 @@ def _add_transformation_block(self, to_block): def _transform_disjunctionData( self, obj, index, parent_disjunct, local_vars_by_disjunct ): - print("Transforming Disjunction %s" % obj) # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -459,7 +458,6 @@ def _transform_disjunctionData( # create one more disaggregated var idx = len(disaggregatedVars) disaggregated_var = disaggregatedVars[idx] - print("Creating extra disaggregated var: '%s'" % disaggregated_var) # mark this as local because we won't re-disaggregate it if this # is a nested disjunction if parent_local_var_list is not None: @@ -550,7 +548,6 @@ def _transform_disjunct( parent_disjunct_local_vars, disjunct_disaggregated_var_map, ): - print("\nTransforming Disjunct '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -585,11 +582,6 @@ def _transform_disjunct( disaggregatedVarName + "_bounds", bigmConstraint ) - print( - "Adding bounds constraints for '%s', the disaggregated var " - "corresponding to Var '%s' on Disjunct '%s'" - % (disaggregatedVar, var, obj) - ) self._declare_disaggregated_var_bounds( original_var=var, disaggregatedVar=disaggregatedVar, @@ -623,7 +615,6 @@ def _transform_disjunct( parent_block = var.parent_block() - print("Adding bounds constraints for local var '%s'" % var) self._declare_disaggregated_var_bounds( original_var=var, disaggregatedVar=var, @@ -702,10 +693,6 @@ def _declare_disaggregated_var_bounds( # the transformation block if disjunct not in disaggregated_var_map: disaggregated_var_map[disjunct] = ComponentMap() - print( - "DISAGGREGATED VAR MAP (%s, %s) : %s" - % (disjunct, original_var, disaggregatedVar) - ) disaggregated_var_map[disjunct][original_var] = disaggregatedVar original_var_map[disaggregatedVar] = original_var From f12b76290f74ea257a210ae3deeb5af7fd91fe08 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:24:23 -0700 Subject: [PATCH 0362/1178] Removing more debugging --- pyomo/gdp/plugins/hull.py | 2 -- pyomo/gdp/tests/test_hull.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 53dffc7a18e..12665eef340 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -55,8 +55,6 @@ logger = logging.getLogger('pyomo.gdp.hull') -from pytest import set_trace - @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index aef119c0f1e..e45a7543e25 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -52,8 +52,6 @@ import os from os.path import abspath, dirname, join -##DEBUG -from pytest import set_trace currdir = dirname(abspath(__file__)) from filecmp import cmp From fed34aedb900160a94261fe448d3e562b8a21fc0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:32:03 -0700 Subject: [PATCH 0363/1178] NFC: updating docstring and removing comments --- pyomo/gdp/plugins/hull.py | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 12665eef340..8813cc25137 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -80,19 +80,11 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): list of blocks and Disjunctions [default: the instance] The transformation will create a new Block with a unique - name beginning "_pyomo_gdp_hull_reformulation". - The block will have a dictionary "_disaggregatedVarMap: - 'srcVar': ComponentMap(:), - 'disaggregatedVar': ComponentMap(:) - - It will also have a ComponentMap "_bigMConstraintMap": - - : - - Last, it will contain an indexed Block named "relaxedDisjuncts", - which will hold the relaxed disjuncts. This block is indexed by - an integer indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": + name beginning "_pyomo_gdp_hull_reformulation". It will contain an + indexed Block named "relaxedDisjuncts" that will hold the relaxed + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. Each block has a dictionary + "_constraintMap": 'srcConstraints': ComponentMap(: ), @@ -108,7 +100,6 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): The _pyomo_gdp_hull_reformulation block will have a ComponentMap "_disaggregationConstraintMap": :ComponentMap(: ) - """ CONFIG = cfg.ConfigDict('gdp.hull') @@ -244,23 +235,6 @@ def _get_user_defined_local_vars(self, targets): blk = blk.parent_block() return user_defined_local_vars - # def _get_local_vars_from_suffixes(self, block, local_var_dict): - # # You can specify suffixes on any block (disjuncts included). This - # # method starts from a Disjunct (presumably) and checks for a LocalVar - # # suffixes going both up and down the tree, adding them into the - # # dictionary that is the second argument. - - # # first look beneath where we are (there could be Blocks on this - # # disjunct) - # for b in block.component_data_objects( - # Block, descend_into=Block, active=True, sort=SortComponents.deterministic - # ): - # self._collect_local_vars_from_block(b, local_var_dict) - # # now traverse upwards and get what's above - # while block is not None: - # self._collect_local_vars_from_block(block, local_var_dict) - # block = block.parent_block() - def _apply_to(self, instance, **kwds): try: self._apply_to_impl(instance, **kwds) From 9caba527f53b101429edd0471fb70647e5df94fb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 20:31:58 -0700 Subject: [PATCH 0364/1178] Updating baselines because I changed the transformation order --- pyomo/gdp/tests/jobshop_large_hull.lp | 1778 ++++++++++++------------- pyomo/gdp/tests/jobshop_small_hull.lp | 122 +- 2 files changed, 950 insertions(+), 950 deletions(-) diff --git a/pyomo/gdp/tests/jobshop_large_hull.lp b/pyomo/gdp/tests/jobshop_large_hull.lp index df3833bdee3..ee8ee0a73d2 100644 --- a/pyomo/gdp/tests/jobshop_large_hull.lp +++ b/pyomo/gdp/tests/jobshop_large_hull.lp @@ -42,87 +42,87 @@ c_u_Feas(G)_: <= -17 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(6)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(7)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(8)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(9)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(10)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(11)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: @@ -132,81 +132,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(15)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(16)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(17)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(18)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(22)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(23)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(24)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(26)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(27)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: @@ -216,33 +216,33 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(29)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(30)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(31)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(32)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(34)_: @@ -258,27 +258,27 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(35)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(36)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(37)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(38)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(39)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: @@ -288,81 +288,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(41)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(42)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(44)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(45)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(46)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(47)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(48)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(49)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(50)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(52)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(53)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: @@ -372,9 +372,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(55)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: @@ -384,81 +384,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(57)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(58)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(60)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(61)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(62)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(63)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(64)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(66)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(67)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(68)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(69)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: @@ -637,546 +637,544 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(F_G_4)_: = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_B_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ -+7.0 NoClash(E_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++2.0 NoClash(A_B_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --1 NoClash(E_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ ++3.0 NoClash(A_B_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_G_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ ++6.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ -+4.0 NoClash(E_G_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++3.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ -+3.0 NoClash(E_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ ++10.0 NoClash(A_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ ++7.0 NoClash(A_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ -+6.0 NoClash(D_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_transformedConstraints(c_0_ub)_: +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ -+1 NoClash(D_F_4_0)_binary_indicator_var ++2.0 NoClash(A_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_transformedConstraints(c_0_ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ -+7.0 NoClash(D_F_4_1)_binary_indicator_var ++3.0 NoClash(A_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 NoClash(D_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_0)_binary_indicator_var +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ -+11.0 NoClash(D_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ ++6.0 NoClash(A_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_1)_binary_indicator_var +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ -+2.0 NoClash(D_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ ++9.0 NoClash(A_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ -+9.0 NoClash(D_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-3.0 NoClash(A_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ -+4.0 NoClash(D_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ ++9.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-3.0 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ -+4.0 NoClash(C_G_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ ++8.0 NoClash(B_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ -+7.0 NoClash(C_G_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ ++3.0 NoClash(B_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ ++10.0 NoClash(B_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-1 NoClash(B_D_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_F_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ ++4.0 NoClash(B_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ -+8.0 NoClash(C_F_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ ++3.0 NoClash(B_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_F_1_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ ++7.0 NoClash(B_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ -+6.0 NoClash(C_F_1_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --2.0 NoClash(C_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_E_5_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_0)_binary_indicator_var +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_1)_binary_indicator_var +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_D_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ ++4.0 NoClash(B_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ ++8.0 NoClash(B_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_D_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ ++3.0 NoClash(B_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_transformedConstraints(c_0_ub)_: @@ -1212,544 +1210,546 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_D_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-2.0 NoClash(C_E_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_0)_binary_indicator_var +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_E_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_1)_binary_indicator_var +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ -+7.0 NoClash(B_E_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ ++2.0 NoClash(C_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ ++6.0 NoClash(C_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_E_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_F_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ ++8.0 NoClash(C_F_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ -+10.0 NoClash(B_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --1 NoClash(B_D_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_D_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ ++4.0 NoClash(C_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_D_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ ++7.0 NoClash(C_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ -+9.0 NoClash(B_C_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ ++4.0 NoClash(D_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --3.0 NoClash(B_C_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ ++8.0 NoClash(D_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ -+9.0 NoClash(A_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ ++2.0 NoClash(D_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --3.0 NoClash(A_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ ++9.0 NoClash(D_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 NoClash(D_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_0)_binary_indicator_var +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ ++11.0 NoClash(D_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_1)_binary_indicator_var +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_F_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ ++1 NoClash(D_F_4_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_0)_binary_indicator_var +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_F_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ ++7.0 NoClash(D_F_4_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_1)_binary_indicator_var +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ ++8.0 NoClash(D_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ ++8.0 NoClash(D_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ -+7.0 NoClash(A_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ ++6.0 NoClash(D_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ -+10.0 NoClash(A_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ ++3.0 NoClash(E_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ ++8.0 NoClash(E_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_C_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ ++8.0 NoClash(E_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_C_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ ++4.0 NoClash(E_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_B_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ ++7.0 NoClash(E_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_B_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-1 NoClash(E_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_B_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 bounds @@ -1761,146 +1761,146 @@ bounds 0 <= t(E) <= 92 0 <= t(F) <= 92 0 <= t(G) <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ <= 92 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_B_5_0)_binary_indicator_var <= 1 diff --git a/pyomo/gdp/tests/jobshop_small_hull.lp b/pyomo/gdp/tests/jobshop_small_hull.lp index c07b9cd048e..ae2d738d29c 100644 --- a/pyomo/gdp/tests/jobshop_small_hull.lp +++ b/pyomo/gdp/tests/jobshop_small_hull.lp @@ -22,29 +22,29 @@ c_u_Feas(C)_: <= -6 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: +1 t(A) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ @@ -52,9 +52,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: @@ -73,98 +73,98 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -+6.0 NoClash(B_C_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_0)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_0)_binary_indicator_var +-19 NoClash(A_B_3_0)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ -+1 NoClash(B_C_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_1)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_1)_binary_indicator_var +-19 NoClash(A_B_3_1)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +2.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +5.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ ++6.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_0)_binary_indicator_var +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_1)_binary_indicator_var +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 bounds @@ -172,18 +172,18 @@ bounds 0 <= t(A) <= 19 0 <= t(B) <= 19 0 <= t(C) <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 19 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 From 8ce0ce9657aac63f3013e9c419eec2a5870e3248 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 20:56:12 -0700 Subject: [PATCH 0365/1178] Changing FME tests that use hull, because I changed the order of transformation --- .../contrib/fme/tests/test_fourier_motzkin_elimination.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index 11c008acf82..e997e138724 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -435,7 +435,7 @@ def check_hull_projected_constraints(self, m, constraints, indices): self.assertIs(body.linear_vars[2], m.startup.binary_indicator_var) self.assertEqual(body.linear_coefs[2], 2) - # 1 <= time1_disjuncts[0].ind_var + time_1.disjuncts[1].ind_var + # 1 <= time1_disjuncts[0].ind_var + time1_disjuncts[1].ind_var cons = constraints[indices[7]] self.assertEqual(cons.lower, 1) self.assertIsNone(cons.upper) @@ -548,12 +548,12 @@ def test_project_disaggregated_vars(self): # we of course get tremendous amounts of garbage, but we make sure that # what should be here is: self.check_hull_projected_constraints( - m, constraints, [23, 19, 8, 10, 54, 67, 35, 3, 4, 1, 2] + m, constraints, [16, 12, 69, 71, 47, 60, 28, 1, 2, 3, 4] ) # and when we filter, it's still there. constraints = filtered._pyomo_contrib_fme_transformation.projected_constraints self.check_hull_projected_constraints( - filtered, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + filtered, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) @unittest.skipIf(not 'glpk' in solvers, 'glpk not available') @@ -570,7 +570,7 @@ def test_post_processing(self): # They should be the same as the above, but now these are *all* the # constraints self.check_hull_projected_constraints( - m, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + m, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) # and check that we didn't change the model From 353939782eb0896527598253b35fd8a23a7d948f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 16:29:55 -0500 Subject: [PATCH 0366/1178] Make `IsInstance` module qualifiers optional --- pyomo/common/config.py | 29 +++++++++++++++++++++++------ pyomo/common/tests/test_config.py | 25 +++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 92613266885..4207392389a 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -310,11 +310,16 @@ class IsInstance(object): ---------- *bases : tuple of type Valid types. + document_full_base_names : bool, optional + True to prepend full module qualifier to the name of each + member of `bases` in ``self.domain_name()`` and/or any + error messages generated by this object, False otherwise. """ - def __init__(self, *bases): + def __init__(self, *bases, document_full_base_names=False): assert bases self.baseClasses = bases + self.document_full_base_names = document_full_base_names @staticmethod def _fullname(klass): @@ -325,29 +330,41 @@ def _fullname(klass): module_qual = "" if module_name == "builtins" else f"{module_name}." return f"{module_qual}{klass.__name__}" + def _get_class_name(self, klass): + """ + Get name of class. Module qualifier may be included, + depending on value of `self.document_full_base_names`. + """ + if self.document_full_base_names: + return self._fullname(klass) + else: + return klass.__name__ + def __call__(self, obj): if isinstance(obj, self.baseClasses): return obj if len(self.baseClasses) > 1: class_names = ", ".join( - f"{self._fullname(kls)!r}" for kls in self.baseClasses + f"{self._get_class_name(kls)!r}" for kls in self.baseClasses ) msg = ( "Expected an instance of one of these types: " f"{class_names}, but received value {obj!r} of type " - f"{self._fullname(type(obj))!r}" + f"{self._get_class_name(type(obj))!r}" ) else: msg = ( f"Expected an instance of " - f"{self._fullname(self.baseClasses[0])!r}, " - f"but received value {obj!r} of type {self._fullname(type(obj))!r}" + f"{self._get_class_name(self.baseClasses[0])!r}, " + f"but received value {obj!r} of type " + f"{self._get_class_name(type(obj))!r}" ) raise ValueError(msg) def domain_name(self): + class_names = (self._get_class_name(kls) for kls in self.baseClasses) return ( - f"IsInstance({', '.join(self._fullname(kls) for kls in self.baseClasses)})" + f"IsInstance({', '.join(class_names)})" ) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 068017d836f..f3f5cbedad6 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -469,13 +469,18 @@ def __repr__(self): c.val2 = testinst self.assertEqual(c.val2, testinst) exc_str = ( - r"Expected an instance of '.*\.TestClass', " + r"Expected an instance of 'TestClass', " "but received value 2.4 of type 'float'" ) with self.assertRaisesRegex(ValueError, exc_str): c.val2 = 2.4 - c.declare("val3", ConfigValue(None, IsInstance(int, TestClass))) + c.declare( + "val3", + ConfigValue( + None, IsInstance(int, TestClass, document_full_base_names=True) + ), + ) self.assertRegex( c.get("val3").domain_name(), r"IsInstance\(int, .*\.TestClass\)" ) @@ -488,6 +493,22 @@ def __repr__(self): with self.assertRaisesRegex(ValueError, exc_str): c.val3 = 2.4 + c.declare( + "val4", + ConfigValue( + None, IsInstance(int, TestClass, document_full_base_names=False) + ), + ) + self.assertEqual(c.get("val4").domain_name(), "IsInstance(int, TestClass)") + c.val4 = 2 + self.assertEqual(c.val4, 2) + exc_str = ( + r"Expected an instance of one of these types: 'int', 'TestClass'" + r", but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val4 = 2.4 + def test_Path(self): def norm(x): if cwd[1] == ':' and x[0] == '/': From bbba7629703ec3461fd8287a2c4bc6e26e29a558 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 16:35:35 -0500 Subject: [PATCH 0367/1178] Add `IsInstance` to config library reference docs --- doc/OnlineDocs/library_reference/common/config.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/OnlineDocs/library_reference/common/config.rst b/doc/OnlineDocs/library_reference/common/config.rst index 7a400b26ce3..c5dc607977a 100644 --- a/doc/OnlineDocs/library_reference/common/config.rst +++ b/doc/OnlineDocs/library_reference/common/config.rst @@ -36,6 +36,7 @@ Domain validators NonPositiveFloat NonNegativeFloat In + IsInstance InEnum ListOf Module @@ -75,6 +76,7 @@ Domain validators .. autofunction:: NonPositiveFloat .. autofunction:: NonNegativeFloat .. autoclass:: In +.. autoclass:: IsInstance .. autoclass:: InEnum .. autoclass:: ListOf .. autoclass:: Module From 0fc42f1923a68259f362c40227fd8da7c78b5b94 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 16:40:18 -0500 Subject: [PATCH 0368/1178] Implement `Path.domain_name()` --- pyomo/common/config.py | 3 +++ pyomo/common/tests/test_config.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 4207392389a..8ffb162ac41 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -555,6 +555,9 @@ def __call__(self, path): ) return ans + def domain_name(self): + return type(self).__name__ + class PathList(Path): """Domain validator for a list of path-like objects. diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index f3f5cbedad6..6c657e8d04b 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -526,6 +526,8 @@ def __str__(self): path_str = str(self.path) return f"{type(self).__name__}({path_str})" + self.assertEqual(Path().domain_name(), "Path") + cwd = os.getcwd() + os.path.sep c = ConfigDict() From 7df175f4d1fd29ce65d37a3d3bed0dabb563d458 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:09:58 -0700 Subject: [PATCH 0369/1178] Fixing BigM to not assume nested indicator vars are local, editing its tests accordingly --- pyomo/gdp/plugins/bigm.py | 5 +- .../gdp/plugins/gdp_to_mip_transformation.py | 32 ++--- pyomo/gdp/tests/test_bigm.py | 117 ++++++++---------- 3 files changed, 75 insertions(+), 79 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index bdd353a6136..1f9f561b192 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -224,11 +224,10 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, bigM, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 96d97206c97..5603259a278 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -213,21 +213,25 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): "likely indicative of a modeling error." % obj.name ) - # Create or fetch the transformation block + # We always need to create or fetch a transformation block on the parent block. + trans_block, new_block = self._add_transformation_block( + obj.parent_block()) + # This is where we put exactly_one/or constraint + algebraic_constraint = self._add_xor_constraint(obj.parent_component(), + trans_block) + + # If requested, create or fetch the transformation block above the + # nested hierarchy if root_disjunct is not None: - # We want to put all the transformed things on the root - # Disjunct's parent's block so that they do not get - # re-transformed - transBlock, new_block = self._add_transformation_block( - root_disjunct.parent_block() - ) - else: - # This isn't nested--just put it on the parent block. - transBlock, new_block = self._add_transformation_block(obj.parent_block()) - - xorConstraint = self._add_xor_constraint(obj.parent_component(), transBlock) - - return transBlock, xorConstraint + # We want to put some transformed things on the root Disjunct's + # parent's block so that they do not get re-transformed. (Note this + # is never true for hull, but it calls this method with + # root_disjunct=None. BigM can't put the exactly-one constraint up + # here, but it can put everything else.) + trans_block, new_block = self._add_transformation_block( + root_disjunct.parent_block() ) + + return trans_block, algebraic_constraint def _get_disjunct_transformation_block(self, disjunct, transBlock): if disjunct.transformation_block is not None: diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index d518219eabd..f210f3cd660 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -33,6 +33,7 @@ assertExpressionsStructurallyEqual, ) from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.common.log import LoggingIntercept import logging @@ -1764,22 +1765,19 @@ def test_transformation_block_structure(self): # we have the XOR constraints for both the outer and inner disjunctions self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - def test_transformation_block_on_inner_disjunct_empty(self): - m = models.makeNestedDisjunctions() - TransformationFactory('gdp.bigm').apply_to(m) - self.assertIsNone(m.disjunct[1].component("_pyomo_gdp_bigm_reformulation")) - def test_mappings_between_disjunctions_and_xors(self): m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.bigm') transform.apply_to(m) transBlock1 = m.component("_pyomo_gdp_bigm_reformulation") + transBlock2 = m.disjunct[1].component("_pyomo_gdp_bigm_reformulation") + transBlock3 = m.simpledisjunct.component("_pyomo_gdp_bigm_reformulation") disjunctionPairs = [ (m.disjunction, transBlock1.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock1.innerdisjunction_xor_4[0]), - (m.simpledisjunct.innerdisjunction, transBlock1.innerdisjunction_xor), + (m.disjunct[1].innerdisjunction[0], transBlock2.innerdisjunction_xor[0]), + (m.simpledisjunct.innerdisjunction, transBlock3.innerdisjunction_xor), ] # check disjunction mappings @@ -1900,18 +1898,39 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): ct.check_linear_coef(self, repn, indicator_var, M) def check_inner_xor_constraint( - self, inner_disjunction, outer_disjunct, inner_disjuncts + self, inner_disjunction, outer_disjunct, bigm ): - self.assertIsNotNone(inner_disjunction.algebraic_constraint) - cons = inner_disjunction.algebraic_constraint - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - for disj in inner_disjuncts: - ct.check_linear_coef(self, repn, disj.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, outer_disjunct.binary_indicator_var, -1) + inner_xor = inner_disjunction.algebraic_constraint + sum_indicators = sum(d.binary_indicator_var for d in + inner_disjunction.disjuncts) + assertExpressionsEqual( + self, + inner_xor.expr, + sum_indicators == 1 + ) + # this guy has been transformed + self.assertFalse(inner_xor.active) + cons = bigm.get_transformed_constraints(inner_xor) + self.assertEqual(len(cons), 2) + lb = cons[0] + ct.check_obj_in_active_tree(self, lb) + lb_expr = self.simplify_cons(lb, leq=False) + assertExpressionsEqual( + self, + lb_expr, + 1.0 <= + sum_indicators + - outer_disjunct.binary_indicator_var + 1.0 + ) + ub = cons[1] + ct.check_obj_in_active_tree(self, ub) + ub_expr = self.simplify_cons(ub, leq=True) + assertExpressionsEqual( + self, + ub_expr, + sum_indicators + + outer_disjunct.binary_indicator_var - 1 <= 1.0 + ) def test_transformed_constraints(self): # We'll check all the transformed constraints to make sure @@ -1993,26 +2012,8 @@ def test_transformed_constraints(self): # Here we check that the xor constraint from # simpledisjunct.innerdisjunction is transformed. - cons5 = m.simpledisjunct.innerdisjunction.algebraic_constraint - self.assertIsNotNone(cons5) - self.check_inner_xor_constraint( - m.simpledisjunct.innerdisjunction, - m.simpledisjunct, - [m.simpledisjunct.innerdisjunct0, m.simpledisjunct.innerdisjunct1], - ) - self.assertIsInstance(cons5, Constraint) - self.assertEqual(cons5.lower, 0) - self.assertEqual(cons5.upper, 0) - repn = generate_standard_repn(cons5.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct0.binary_indicator_var, 1 - ) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct1.binary_indicator_var, 1 - ) - ct.check_linear_coef(self, repn, m.simpledisjunct.binary_indicator_var, -1) + self.check_inner_xor_constraint(m.simpledisjunct.innerdisjunction, + m.simpledisjunct, bigm) cons6 = bigm.get_transformed_constraints(m.disjunct[0].c) self.assertEqual(len(cons6), 2) @@ -2029,8 +2030,7 @@ def test_transformed_constraints(self): # is correct. self.check_inner_xor_constraint( m.disjunct[1].innerdisjunction[0], - m.disjunct[1], - [m.disjunct[1].innerdisjunct[0], m.disjunct[1].innerdisjunct[1]], + m.disjunct[1], bigm ) cons8 = bigm.get_transformed_constraints(m.disjunct[1].c) @@ -2136,34 +2136,27 @@ def check_second_disjunct_constraint(self, disj2c, x, ind_var): ct.check_squared_term_coef(self, repn, x[i], 1) ct.check_linear_coef(self, repn, x[i], -6) + def simplify_cons(self, cons, leq): + visitor = LinearRepnVisitor({}, {}, {}, None) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + if leq: + self.assertIsNone(cons.lower) + ub = cons.upper + return ub >= repn.to_expression(visitor) + else: + self.assertIsNone(cons.upper) + lb = cons.lower + return lb <= repn.to_expression(visitor) + def check_hierarchical_nested_model(self, m, bigm): outer_xor = m.disjunction_block.disjunction.algebraic_constraint ct.check_two_term_disjunction_xor( self, outer_xor, m.disj1, m.disjunct_block.disj2 ) - inner_xor = m.disjunct_block.disj2.disjunction.algebraic_constraint - self.assertEqual(inner_xor.lower, 0) - self.assertEqual(inner_xor.upper, 0) - repn = generate_standard_repn(inner_xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(len(repn.linear_vars), 3) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, repn, m.disjunct_block.disj2.binary_indicator_var, -1 - ) + self.check_inner_xor_constraint(m.disjunct_block.disj2.disjunction, + m.disjunct_block.disj2, bigm) # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) From 1e8f359ad5cf8c4c14f2f7b3a49584ca9cbc0a56 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:10:45 -0700 Subject: [PATCH 0370/1178] Black --- .../gdp/plugins/gdp_to_mip_transformation.py | 11 ++++--- pyomo/gdp/tests/test_bigm.py | 33 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 5603259a278..59cb221321a 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -214,11 +214,11 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): ) # We always need to create or fetch a transformation block on the parent block. - trans_block, new_block = self._add_transformation_block( - obj.parent_block()) + trans_block, new_block = self._add_transformation_block(obj.parent_block()) # This is where we put exactly_one/or constraint - algebraic_constraint = self._add_xor_constraint(obj.parent_component(), - trans_block) + algebraic_constraint = self._add_xor_constraint( + obj.parent_component(), trans_block + ) # If requested, create or fetch the transformation block above the # nested hierarchy @@ -229,7 +229,8 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): # root_disjunct=None. BigM can't put the exactly-one constraint up # here, but it can put everything else.) trans_block, new_block = self._add_transformation_block( - root_disjunct.parent_block() ) + root_disjunct.parent_block() + ) return trans_block, algebraic_constraint diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index f210f3cd660..daec9a20c93 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1897,17 +1897,12 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): ct.check_linear_coef(self, repn, variable, 1) ct.check_linear_coef(self, repn, indicator_var, M) - def check_inner_xor_constraint( - self, inner_disjunction, outer_disjunct, bigm - ): + def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): inner_xor = inner_disjunction.algebraic_constraint - sum_indicators = sum(d.binary_indicator_var for d in - inner_disjunction.disjuncts) - assertExpressionsEqual( - self, - inner_xor.expr, - sum_indicators == 1 + sum_indicators = sum( + d.binary_indicator_var for d in inner_disjunction.disjuncts ) + assertExpressionsEqual(self, inner_xor.expr, sum_indicators == 1) # this guy has been transformed self.assertFalse(inner_xor.active) cons = bigm.get_transformed_constraints(inner_xor) @@ -1918,9 +1913,7 @@ def check_inner_xor_constraint( assertExpressionsEqual( self, lb_expr, - 1.0 <= - sum_indicators - - outer_disjunct.binary_indicator_var + 1.0 + 1.0 <= sum_indicators - outer_disjunct.binary_indicator_var + 1.0, ) ub = cons[1] ct.check_obj_in_active_tree(self, ub) @@ -1928,8 +1921,7 @@ def check_inner_xor_constraint( assertExpressionsEqual( self, ub_expr, - sum_indicators - + outer_disjunct.binary_indicator_var - 1 <= 1.0 + sum_indicators + outer_disjunct.binary_indicator_var - 1 <= 1.0, ) def test_transformed_constraints(self): @@ -2012,8 +2004,9 @@ def test_transformed_constraints(self): # Here we check that the xor constraint from # simpledisjunct.innerdisjunction is transformed. - self.check_inner_xor_constraint(m.simpledisjunct.innerdisjunction, - m.simpledisjunct, bigm) + self.check_inner_xor_constraint( + m.simpledisjunct.innerdisjunction, m.simpledisjunct, bigm + ) cons6 = bigm.get_transformed_constraints(m.disjunct[0].c) self.assertEqual(len(cons6), 2) @@ -2029,8 +2022,7 @@ def test_transformed_constraints(self): # now we check that the xor constraint from disjunct[1].innerdisjunction # is correct. self.check_inner_xor_constraint( - m.disjunct[1].innerdisjunction[0], - m.disjunct[1], bigm + m.disjunct[1].innerdisjunction[0], m.disjunct[1], bigm ) cons8 = bigm.get_transformed_constraints(m.disjunct[1].c) @@ -2155,8 +2147,9 @@ def check_hierarchical_nested_model(self, m, bigm): self, outer_xor, m.disj1, m.disjunct_block.disj2 ) - self.check_inner_xor_constraint(m.disjunct_block.disj2.disjunction, - m.disjunct_block.disj2, bigm) + self.check_inner_xor_constraint( + m.disjunct_block.disj2.disjunction, m.disjunct_block.disj2, bigm + ) # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) From 263d873c195d9a469b79818cf7d1fcc422c6e492 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:18:29 -0700 Subject: [PATCH 0371/1178] Fixing the algebraic constraint for mbigm to be correct for nested GDPs which is ironic because mbigm doesn't currently support nested GDPs. --- pyomo/gdp/plugins/multiple_bigm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index a2e7d5beeec..6177de3c037 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -336,8 +336,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, root_disjunct) for disjunct in active_disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() self._transform_disjunct(disjunct, transBlock, active_disjuncts, Ms) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - algebraic_constraint.add(index, (or_expr, rhs)) + algebraic_constraint.add(index, or_expr == 1) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(algebraic_constraint[index]) From 664f3026181ac51f01e5d65abff03fc89b424cf0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:21:24 -0700 Subject: [PATCH 0372/1178] Correcting binary multiplication transformation's handling of nested GDP --- pyomo/gdp/plugins/binary_multiplication.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index ef4239e09dc..d68f7efe76f 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -92,11 +92,10 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) From 019818456cea31153366f8b11f5e69465e226770 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 18:37:14 -0500 Subject: [PATCH 0373/1178] Update documentation of `Path` --- pyomo/common/config.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 8ffb162ac41..8f5ac513ee4 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -490,9 +490,14 @@ def __call__(self, module_id): class Path(object): - """Domain validator for path-like options. + """ + Domain validator for a + :py:term:`path-like object `. - This will admit any object and convert it to a string. It will then + This will admit a path-like object + and get the object's file system representation + through :py:obj:`os.fsdecode`. + It will then expand any environment variables and leading usernames (e.g., "~myuser" or "~/") appearing in either the value or the base path before concatenating the base path and value, expanding the path to From e4d9d796b45701982c4643b0e5e4ee064893f48a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:42:38 -0700 Subject: [PATCH 0374/1178] Adding an integration test for correct handling of nested with non-local indicator vars --- pyomo/gdp/tests/common_tests.py | 13 ++++++++ pyomo/gdp/tests/models.py | 32 +++++++++++++++++++ pyomo/gdp/tests/test_bigm.py | 4 +++ pyomo/gdp/tests/test_binary_multiplication.py | 11 +++++++ pyomo/gdp/tests/test_hull.py | 4 +++ 5 files changed, 64 insertions(+) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index b76de61887f..585aafc967d 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1944,3 +1944,16 @@ def check_nested_disjuncts_in_flat_gdp(self, transformation): for t in m.T: self.assertTrue(value(m.disj1[t].indicator_var)) self.assertTrue(value(m.disj1[t].sub1.indicator_var)) + +def check_do_not_assume_nested_indicators_local(self, transformation): + m = models.why_indicator_vars_are_not_always_local() + TransformationFactory(transformation).apply_to(m) + + results = SolverFactory('gurobi').solve(m) + self.assertEqual(results.solver.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(value(m.obj), 9) + self.assertAlmostEqual(value(m.x), 9) + self.assertTrue(value(m.Y2.indicator_var)) + self.assertFalse(value(m.Y1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index 94bf5d0e592..fc5e6327c7e 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -563,6 +563,38 @@ def makeNestedDisjunctions_NestedDisjuncts(): return m +def why_indicator_vars_are_not_always_local(): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + @m.Disjunct() + def Z1(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.1) + @m.Disjunct() + def Z2(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.2) + @m.Disjunct() + def Y1(d): + m = d.model() + d.c = Constraint(expr=(1.15, m.x, 8)) + d.disjunction = Disjunction(expr=[m.Z1, m.Z2]) + @m.Disjunct() + def Y2(d): + m = d.model() + d.c = Constraint(expr=m.x==9) + m.disjunction = Disjunction(expr=[m.Y1, m.Y2]) + + m.logical_cons = LogicalConstraint(expr=m.Y2.indicator_var.implies( + m.Z1.indicator_var.land(m.Z2.indicator_var))) + + # optimal value is 9, but it will be 8 if we wrongly assume that the nested + # indicator_vars are local. + m.obj = Objective(expr=m.x, sense=maximize) + + return m + + def makeTwoSimpleDisjunctions(): """Two SimpleDisjunctions on the same model.""" m = ConcreteModel() diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index daec9a20c93..00efcb46485 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -2200,6 +2200,10 @@ def test_decl_order_opposite_instantiation_order(self): # the same check to make sure everything is transformed correctly. self.check_hierarchical_nested_model(m, bigm) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.bigm') + class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 5f4c4f90ab6..fbe6f86fd46 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -18,6 +18,7 @@ ConcreteModel, Var, Any, + SolverFactory, ) from pyomo.gdp import Disjunct, Disjunction from pyomo.core.expr.compare import assertExpressionsEqual @@ -30,6 +31,11 @@ import random +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() +) + class CommonTests: def diff_apply_to_and_create_using(self, model): @@ -297,5 +303,10 @@ def test_local_var(self): self.assertEqual(eq.ub, 0) +class TestNestedGDP(unittest.TestCase): + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.binary_multiplication') + if __name__ == '__main__': unittest.main() diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 694178ee96f..858764759ee 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -2030,6 +2030,10 @@ def test_nested_with_var_that_skips_a_level(self): cons_expr = self.simplify_cons(cons) assertExpressionsEqual(self, cons_expr, m.y - y_y2 - y_y1 == 0.0) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.hull') + class TestSpecialCases(unittest.TestCase): def test_local_vars(self): From d97f4ec7d2eeaa441643195aa6a665d1bc53b9da Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:43:08 -0700 Subject: [PATCH 0375/1178] weighing in with black --- pyomo/gdp/tests/common_tests.py | 1 + pyomo/gdp/tests/models.py | 12 +++++++++--- pyomo/gdp/tests/test_binary_multiplication.py | 5 ++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 585aafc967d..28025816262 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1945,6 +1945,7 @@ def check_nested_disjuncts_in_flat_gdp(self, transformation): self.assertTrue(value(m.disj1[t].indicator_var)) self.assertTrue(value(m.disj1[t].sub1.indicator_var)) + def check_do_not_assume_nested_indicators_local(self, transformation): m = models.why_indicator_vars_are_not_always_local() TransformationFactory(transformation).apply_to(m) diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index fc5e6327c7e..0b84641899c 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -566,27 +566,33 @@ def makeNestedDisjunctions_NestedDisjuncts(): def why_indicator_vars_are_not_always_local(): m = ConcreteModel() m.x = Var(bounds=(1, 10)) + @m.Disjunct() def Z1(d): m = d.model() d.c = Constraint(expr=m.x >= 1.1) + @m.Disjunct() def Z2(d): m = d.model() d.c = Constraint(expr=m.x >= 1.2) + @m.Disjunct() def Y1(d): m = d.model() d.c = Constraint(expr=(1.15, m.x, 8)) d.disjunction = Disjunction(expr=[m.Z1, m.Z2]) + @m.Disjunct() def Y2(d): m = d.model() - d.c = Constraint(expr=m.x==9) + d.c = Constraint(expr=m.x == 9) + m.disjunction = Disjunction(expr=[m.Y1, m.Y2]) - m.logical_cons = LogicalConstraint(expr=m.Y2.indicator_var.implies( - m.Z1.indicator_var.land(m.Z2.indicator_var))) + m.logical_cons = LogicalConstraint( + expr=m.Y2.indicator_var.implies(m.Z1.indicator_var.land(m.Z2.indicator_var)) + ) # optimal value is 9, but it will be 8 if we wrongly assume that the nested # indicator_vars are local. diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index fbe6f86fd46..aa846c4710a 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -306,7 +306,10 @@ def test_local_var(self): class TestNestedGDP(unittest.TestCase): @unittest.skipUnless(gurobi_available, "Gurobi is not available") def test_do_not_assume_nested_indicators_local(self): - ct.check_do_not_assume_nested_indicators_local(self, 'gdp.binary_multiplication') + ct.check_do_not_assume_nested_indicators_local( + self, 'gdp.binary_multiplication' + ) + if __name__ == '__main__': unittest.main() From 4236f63d2afa62bed822b2f49171e13c7bc04650 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 19:17:01 -0500 Subject: [PATCH 0376/1178] Make `PathList` more consistent with `Path` --- pyomo/common/config.py | 27 ++++++++++++++++++--------- pyomo/common/tests/test_config.py | 9 +++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 8f5ac513ee4..1da2e603c7b 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -565,12 +565,16 @@ def domain_name(self): class PathList(Path): - """Domain validator for a list of path-like objects. + """ + Domain validator for a list of + :py:term:`path-like objects `. - This will admit any iterable or object convertible to a string. - Iterable objects (other than strings) will have each member - normalized using :py:class:`Path`. Other types will be passed to - :py:class:`Path`, returning a list with the single resulting path. + This admits a path-like object or iterable of such. + If a path-like object is passed, then + a singleton list containing the object normalized through + :py:class:`Path` is returned. + An iterable of path-like objects is cast to a list, each + entry of which is normalized through :py:class:`Path`. Parameters ---------- @@ -587,10 +591,15 @@ class PathList(Path): """ def __call__(self, data): - if hasattr(data, "__iter__") and not isinstance(data, str): - return [super(PathList, self).__call__(i) for i in data] - else: - return [super(PathList, self).__call__(data)] + try: + pathlist = [super(PathList, self).__call__(data)] + except TypeError as err: + is_not_path_like = ("expected str, bytes or os.PathLike" in str(err)) + if is_not_path_like and hasattr(data, "__iter__"): + pathlist = [super(PathList, self).__call__(i) for i in data] + else: + raise + return pathlist class DynamicImplicitDomain(object): diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 6c657e8d04b..912a9ab1d7c 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -748,6 +748,8 @@ def norm(x): cwd = os.getcwd() + os.path.sep c = ConfigDict() + self.assertEqual(PathList().domain_name(), "PathList") + c.declare('a', ConfigValue(None, PathList())) self.assertEqual(c.a, None) c.a = "/a/b/c" @@ -770,6 +772,13 @@ def norm(x): self.assertEqual(len(c.a), 0) self.assertIs(type(c.a), list) + exc_str = r".*expected str, bytes or os.PathLike.*int" + + with self.assertRaisesRegex(ValueError, exc_str): + c.a = 2 + with self.assertRaisesRegex(ValueError, exc_str): + c.a = ["/a/b/c", 2] + def test_ListOf(self): c = ConfigDict() c.declare('a', ConfigValue(domain=ListOf(int), default=None)) From a07d696447d92fa58085c391809cb6762c94a44f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 19:44:37 -0500 Subject: [PATCH 0377/1178] Apply black --- pyomo/common/config.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 1da2e603c7b..f156bee79a9 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -363,9 +363,7 @@ def __call__(self, obj): def domain_name(self): class_names = (self._get_class_name(kls) for kls in self.baseClasses) - return ( - f"IsInstance({', '.join(class_names)})" - ) + return f"IsInstance({', '.join(class_names)})" class ListOf(object): @@ -594,7 +592,7 @@ def __call__(self, data): try: pathlist = [super(PathList, self).__call__(data)] except TypeError as err: - is_not_path_like = ("expected str, bytes or os.PathLike" in str(err)) + is_not_path_like = "expected str, bytes or os.PathLike" in str(err) if is_not_path_like and hasattr(data, "__iter__"): pathlist = [super(PathList, self).__call__(i) for i in data] else: From 9c1727bbb7e99a3018a9c8afa56384e1e7d27e80 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 08:07:19 -0700 Subject: [PATCH 0378/1178] Save state: config changes --- pyomo/common/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index a796c34340b..d9b495ff1bd 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -211,8 +211,6 @@ def Datetime(val): This domain will return the original object, assuming it is of the right type. """ - if val is None: - return val if not isinstance(val, datetime.datetime): raise ValueError(f"Expected datetime object, but received {type(val)}.") return val From fb6f97e80e507af5418bbacc00c603f48f898920 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 08:53:37 -0700 Subject: [PATCH 0379/1178] Address @jsiirola's comments --- pyomo/common/config.py | 2 +- pyomo/common/formatting.py | 4 +- pyomo/contrib/solver/base.py | 3 - pyomo/contrib/solver/config.py | 6 +- pyomo/contrib/solver/gurobi.py | 7 +- pyomo/contrib/solver/ipopt.py | 40 +++--- pyomo/contrib/solver/results.py | 1 - pyomo/contrib/solver/solution.py | 130 ++++-------------- .../contrib/solver/tests/unit/test_results.py | 99 +++++++++++-- pyomo/opt/plugins/sol.py | 1 - 10 files changed, 141 insertions(+), 152 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 7ece0e6a48c..3e9b580c7bc 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -40,7 +40,6 @@ deprecation_warning, relocated_module_attribute, ) -from pyomo.common.errors import DeveloperError from pyomo.common.fileutils import import_file from pyomo.common.formatting import wrap_reStructuredText from pyomo.common.modeling import NOTSET @@ -767,6 +766,7 @@ def from_enum_or_string(cls, arg): NegativeFloat NonPositiveFloat NonNegativeFloat + Datetime In InEnum IsInstance diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 6194f928844..430ec96ca09 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,8 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' - r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' # line blocks + r'|(\+((-{3,})|(={3,}))\+)' # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index a60e770e660..cb13809c438 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -180,9 +180,6 @@ class PersistentSolverBase(SolverBase): CONFIG = PersistentSolverConfig() - def __init__(self, **kwds): - super().__init__(**kwds) - @document_kwargs_from_configdict(CONFIG) @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index d13e1caf81d..21f6e233d78 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -94,7 +94,11 @@ def __init__( ), ) self.timer: HierarchicalTimer = self.declare( - 'timer', ConfigValue(default=None, description="A HierarchicalTimer.") + 'timer', + ConfigValue( + default=None, + description="A timer object for recording relevant process timing data.", + ), ) self.threads: Optional[int] = self.declare( 'threads', diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index c1b02c08ef9..2b4986edaf8 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -283,11 +283,8 @@ def _check_license(self): if avail: if self._available is None: - res = Gurobi._check_full_license() - self._available = res - return res - else: - return self._available + self._available = Gurobi._check_full_license() + return self._available else: return self.Availability.BadLicense diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index edea4e693b4..0d0d89f837a 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -34,7 +34,7 @@ from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.contrib.solver.sol_reader import parse_sol_file -from pyomo.contrib.solver.solution import SolSolutionLoader, SolutionLoader +from pyomo.contrib.solver.solution import SolSolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions @@ -103,14 +103,14 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[float] = ( + self.timing_info.ipopt_excluding_nlp_functions: Optional[float] = ( self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + 'ipopt_excluding_nlp_functions', ConfigValue(domain=NonNegativeFloat) ) ) - self.timing_info.function_solve_time: Optional[float] = ( + self.timing_info.nlp_function_evaluations: Optional[float] = ( self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + 'nlp_function_evaluations', ConfigValue(domain=NonNegativeFloat) ) ) @@ -225,10 +225,11 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None + self.executable = self.config.executable def available(self): if self._available_cache is None: - if self.config.executable.path() is None: + if self.executable.path() is None: self._available_cache = self.Availability.NotFound else: self._available_cache = self.Availability.FullLicense @@ -237,7 +238,7 @@ def available(self): def version(self): if self._version_cache is None: results = subprocess.run( - [str(self.config.executable), '--version'], + [str(self.executable), '--version'], timeout=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -266,7 +267,7 @@ def _write_options_file(self, filename: str, options: Mapping): return opt_file_exists def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: bool): - cmd = [str(config.executable), basename + '.nl', '-AMPL'] + cmd = [str(self.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: @@ -296,6 +297,7 @@ def solve(self, model, **kwds): ) # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(value=kwds, preserve_implicit=True) + self.executable = config.executable if config.threads: logger.log( logging.WARNING, @@ -306,7 +308,6 @@ def solve(self, model, **kwds): else: timer = config.timer StaleFlagManager.mark_all_as_stale() - results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.working_dir is None: dname = tempfile.mkdtemp() @@ -379,16 +380,18 @@ def solve(self, model, **kwds): ) if process.returncode != 0: + results = ipoptResults() + results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error - results.solution_loader = SolutionLoader(None, None, None) + results.solution_loader = SolSolutionLoader(None, None) else: with open(basename + '.sol', 'r') as sol_file: timer.start('parse_sol') - results = self._parse_solution(sol_file, nl_info, results) + results = self._parse_solution(sol_file, nl_info) timer.stop('parse_sol') results.iteration_count = iters - results.timing_info.no_function_solve_time = ipopt_time_nofunc - results.timing_info.function_solve_time = ipopt_time_func + results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc + results.timing_info.nlp_function_evaluations = ipopt_time_func if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -397,7 +400,7 @@ def solve(self, model, **kwds): 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' ) - results.solver_name = 'ipopt' + results.solver_name = self.name results.solver_version = self.version() if ( config.load_solutions @@ -484,15 +487,14 @@ def _parse_ipopt_output(self, stream: io.StringIO): return iters, nofunc_time, func_time - def _parse_solution( - self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults - ): + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + results = ipoptResults() res, sol_data = parse_sol_file( - sol_file=instream, nl_info=nl_info, result=result + sol_file=instream, nl_info=nl_info, result=results ) if res.solution_status == SolutionStatus.noSolution: - res.solution_loader = SolutionLoader(None, None, None) + res.solution_loader = SolSolutionLoader(None, None) else: res.solution_loader = ipoptSolutionLoader( sol_data=sol_data, nl_info=nl_info diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index b330773e4f3..f2c9cde64fe 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -27,7 +27,6 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) -from pyomo.common.timing import HierarchicalTimer class TerminationCondition(enum.Enum): diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 31792a76dfe..1812e21a596 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ import abc -from typing import Sequence, Dict, Optional, Mapping, MutableMapping, NoReturn +from typing import Sequence, Dict, Optional, Mapping, NoReturn from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData @@ -18,7 +18,6 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.sol_reader import SolFileData from pyomo.repn.plugins.nl_writer import NLWriterInfo -from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions @@ -106,78 +105,33 @@ def get_reduced_costs( ) -# TODO: This is for development uses only; not to be released to the wild -# May turn into documentation someday -class SolutionLoader(SolutionLoaderBase): - def __init__( - self, - primals: Optional[MutableMapping], - duals: Optional[MutableMapping], - reduced_costs: Optional[MutableMapping], - ): - """ - Parameters - ---------- - primals: dict - maps id(Var) to (var, value) - duals: dict - maps Constraint to dual value - reduced_costs: dict - maps id(Var) to (var, reduced_cost) - """ - self._primals = primals - self._duals = duals - self._reduced_costs = reduced_costs +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver): + self._solver = solver + self._valid = True - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - if self._primals is None: - raise RuntimeError( - 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' - ) - if vars_to_load is None: - return ComponentMap(self._primals.values()) - else: - primals = ComponentMap() - for v in vars_to_load: - primals[v] = self._primals[id(v)][1] - return primals + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: - if self._duals is None: - raise RuntimeError( - 'Solution loader does not currently have valid duals. Please ' - 'check the termination condition and ensure the solver returns duals ' - 'for the given problem type.' - ) - if cons_to_load is None: - duals = dict(self._duals) - else: - duals = {} - for c in cons_to_load: - duals[c] = self._duals[c] - return duals + self._assert_solution_still_valid() + return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: - if self._reduced_costs is None: - raise RuntimeError( - 'Solution loader does not currently have valid reduced costs. Please ' - 'check the termination condition and ensure the solver returns reduced ' - 'costs for the given problem type.' - ) - if vars_to_load is None: - rc = ComponentMap(self._reduced_costs.values()) - else: - rc = ComponentMap() - for v in vars_to_load: - rc[v] = self._reduced_costs[id(v)][1] - return rc + self._assert_solution_still_valid() + return self._solver._get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False class SolSolutionLoader(SolutionLoaderBase): @@ -188,17 +142,14 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: - if self._nl_info.scaling is None: - scale_list = [1] * len(self._nl_info.variables) + if self._nl_info.scaling: + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling + ): + v.set_value(val / scale, skip_validation=True) else: - scale_list = self._nl_info.scaling.variables - for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, scale_list - ): - v.set_value(val / scale, skip_validation=True) - - for v, v_expr in self._nl_info.eliminated_vars: - v.set_value(value(v_expr), skip_validation=True) + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) StaleFlagManager.mark_all_as_stale(delayed=True) @@ -248,32 +199,3 @@ def get_duals( if c in cons_to_load: res[c] = val * scale return res - - -class PersistentSolutionLoader(SolutionLoaderBase): - def __init__(self, solver): - self._solver = solver - self._valid = True - - def _assert_solution_still_valid(self): - if not self._valid: - raise RuntimeError('The results in the solver are no longer valid.') - - def get_primals(self, vars_to_load=None): - self._assert_solution_still_valid() - return self._solver._get_primals(vars_to_load=vars_to_load) - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver._get_duals(cons_to_load=cons_to_load) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - self._assert_solution_still_valid() - return self._solver._get_reduced_costs(vars_to_load=vars_to_load) - - def invalidate(self): - self._valid = False diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 2d8f6460448..4856b737295 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -10,15 +10,97 @@ # ___________________________________________________________________________ from io import StringIO +from typing import Sequence, Dict, Optional, Mapping, MutableMapping + from pyomo.common import unittest from pyomo.common.config import ConfigDict +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData +from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results from pyomo.contrib.solver import solution import pyomo.environ as pyo from pyomo.core.base.var import ScalarVar +class SolutionLoaderExample(solution.SolutionLoaderBase): + """ + This is an example instantiation of a SolutionLoader that is used for + testing generated results. + """ + + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = {} + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + class TestTerminationCondition(unittest.TestCase): def test_member_list(self): member_list = results.TerminationCondition._member_names_ @@ -92,6 +174,7 @@ def test_member_list(self): def test_default_initialization(self): res = results.Results() + self.assertIsNone(res.solution_loader) self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) self.assertEqual( @@ -105,20 +188,6 @@ def test_default_initialization(self): self.assertIsInstance(res.extra_info, ConfigDict) self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) - res.solution_loader = solution.SolutionLoader(None, None, None) - - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have a valid solution.*' - ): - res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() def test_display(self): res = results.Results() @@ -160,7 +229,7 @@ def test_generated_results(self): rc[id(m.y)] = (m.y, 6) res = results.Results() - res.solution_loader = solution.SolutionLoader( + res.solution_loader = SolutionLoaderExample( primals=primals, duals=duals, reduced_costs=rc ) diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index a6088cb25af..10da469f186 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -189,7 +189,6 @@ def _load(self, fin, res, soln, suffixes): if line == "": continue line = line.split() - # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes if line[0] != 'suffix': # We assume this is the start of a # section like kestrel_option, which From 544aa459ed9cebc890dd92bfa1601ada19aadb4d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 08:54:22 -0700 Subject: [PATCH 0380/1178] Missed one file --- pyomo/contrib/solver/util.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index d104022692e..ca499748adf 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -153,18 +153,6 @@ def collect_vars_and_named_exprs(expr): ) -class SolverUtils: - pass - - -class SubprocessSolverUtils: - pass - - -class DirectSolverUtils: - pass - - class PersistentSolverUtils(abc.ABC): def __init__(self): self._model = None From 9bf1970ffc2d19c7978a7a5312deb6a68c6f03cc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 09:09:53 -0700 Subject: [PATCH 0381/1178] Remove because it's not implemented and we have a different idea --- pyomo/contrib/solver/config.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 21f6e233d78..9b8de245bd6 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -50,14 +50,6 @@ def __init__( description="If True, the solver log prints to stdout.", ), ) - self.log_solver_output: bool = self.declare( - 'log_solver_output', - ConfigValue( - domain=bool, - default=False, - description="If True, the solver output gets logged.", - ), - ) self.working_dir: str = self.declare( 'working_dir', ConfigValue( From 95cd64ecbbbb51363c92a6e2cdac5a0835c2ce90 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 09:14:46 -0700 Subject: [PATCH 0382/1178] Remove from __init__ --- pyomo/contrib/solver/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/solver/__init__.py b/pyomo/contrib/solver/__init__.py index 2dc73091ea2..a4a626013c4 100644 --- a/pyomo/contrib/solver/__init__.py +++ b/pyomo/contrib/solver/__init__.py @@ -8,9 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -from . import base -from . import config -from . import results -from . import solution -from . import util From c17d2b9da8639641e5ba910014c4a8a653fcd505 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 09:37:05 -0700 Subject: [PATCH 0383/1178] Reverting: log_solver_output does do something --- pyomo/contrib/solver/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 9b8de245bd6..21f6e233d78 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -50,6 +50,14 @@ def __init__( description="If True, the solver log prints to stdout.", ), ) + self.log_solver_output: bool = self.declare( + 'log_solver_output', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver output gets logged.", + ), + ) self.working_dir: str = self.declare( 'working_dir', ConfigValue( From 10e08f922463f915c5566d973dba8d9440335fe9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 10:00:45 -0700 Subject: [PATCH 0384/1178] Update domain validator for bools --- pyomo/contrib/solver/config.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 21f6e233d78..1e9f0b1fbe9 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -17,6 +17,7 @@ NonNegativeFloat, NonNegativeInt, ADVANCED_OPTION, + Bool, ) from pyomo.common.timing import HierarchicalTimer @@ -45,7 +46,7 @@ def __init__( self.tee: bool = self.declare( 'tee', ConfigValue( - domain=bool, + domain=Bool, default=False, description="If True, the solver log prints to stdout.", ), @@ -53,7 +54,7 @@ def __init__( self.log_solver_output: bool = self.declare( 'log_solver_output', ConfigValue( - domain=bool, + domain=Bool, default=False, description="If True, the solver output gets logged.", ), @@ -70,7 +71,7 @@ def __init__( self.load_solutions: bool = self.declare( 'load_solutions', ConfigValue( - domain=bool, + domain=Bool, default=True, description="If True, the values of the primal variables will be loaded into the model.", ), @@ -78,7 +79,7 @@ def __init__( self.raise_exception_on_nonoptimal_result: bool = self.declare( 'raise_exception_on_nonoptimal_result', ConfigValue( - domain=bool, + domain=Bool, default=True, description="If False, the `solve` method will continue processing " "even if the returned result is nonoptimal.", @@ -87,7 +88,7 @@ def __init__( self.symbolic_solver_labels: bool = self.declare( 'symbolic_solver_labels', ConfigValue( - domain=bool, + domain=Bool, default=False, description="If True, the names given to the solver will reflect the names of the Pyomo components. " "Cannot be changed after set_instance is called.", From ca0280c5b50aa09fb0083c753f3b2972ab5c9c50 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 10:03:50 -0700 Subject: [PATCH 0385/1178] update type hints in configs --- pyomo/contrib/solver/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 1e9f0b1fbe9..ca9557d0002 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -59,7 +59,7 @@ def __init__( description="If True, the solver output gets logged.", ), ) - self.working_dir: str = self.declare( + self.working_dir: Optional[str] = self.declare( 'working_dir', ConfigValue( domain=str, @@ -94,7 +94,7 @@ def __init__( "Cannot be changed after set_instance is called.", ), ) - self.timer: HierarchicalTimer = self.declare( + self.timer: Optional[HierarchicalTimer] = self.declare( 'timer', ConfigValue( default=None, From df339688e125abed7996c68df71cc9fcc19117f6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 10:30:19 -0700 Subject: [PATCH 0386/1178] properly copy the nl writer config --- pyomo/contrib/solver/ipopt.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 0d0d89f837a..6fb369d4785 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -80,10 +80,7 @@ def __init__( ) self.writer_config: ConfigDict = self.declare( 'writer_config', - ConfigValue( - default=NLWriter.CONFIG(), - description="For the manipulation of NL writer options.", - ), + NLWriter.CONFIG(), ) From 086d53cd197168665cf6f63fcfc57bcd3b97ea14 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:07:55 -0700 Subject: [PATCH 0387/1178] Apply black; change to ccapital I --- pyomo/contrib/solver/ipopt.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 6fb369d4785..1aa00c0c8e2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -53,7 +53,7 @@ class ipoptSolverError(PyomoException): """ -class ipoptConfig(SolverConfig): +class IpoptConfig(SolverConfig): def __init__( self, description=None, @@ -79,12 +79,11 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', - NLWriter.CONFIG(), + 'writer_config', NLWriter.CONFIG() ) -class ipoptResults(Results): +class IpoptResults(Results): def __init__( self, description=None, @@ -112,7 +111,7 @@ def __init__( ) -class ipoptSolutionLoader(SolSolutionLoader): +class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: @@ -214,8 +213,8 @@ def get_reduced_costs( @SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)') -class ipopt(SolverBase): - CONFIG = ipoptConfig() +class Ipopt(SolverBase): + CONFIG = IpoptConfig() def __init__(self, **kwds): super().__init__(**kwds) @@ -263,7 +262,7 @@ def _write_options_file(self, filename: str, options: Mapping): opt_file.write(str(k) + ' ' + str(val) + '\n') return opt_file_exists - def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: bool): + def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): cmd = [str(self.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') @@ -293,7 +292,7 @@ def solve(self, model, **kwds): f'Solver {self.__class__} is not available ({avail}).' ) # Update configuration options, based on keywords passed to solve - config: ipoptConfig = self.config(value=kwds, preserve_implicit=True) + config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) self.executable = config.executable if config.threads: logger.log( @@ -377,7 +376,7 @@ def solve(self, model, **kwds): ) if process.returncode != 0: - results = ipoptResults() + results = IpoptResults() results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error results.solution_loader = SolSolutionLoader(None, None) @@ -485,7 +484,7 @@ def _parse_ipopt_output(self, stream: io.StringIO): return iters, nofunc_time, func_time def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): - results = ipoptResults() + results = IpoptResults() res, sol_data = parse_sol_file( sol_file=instream, nl_info=nl_info, result=results ) @@ -493,7 +492,7 @@ def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): if res.solution_status == SolutionStatus.noSolution: res.solution_loader = SolSolutionLoader(None, None) else: - res.solution_loader = ipoptSolutionLoader( + res.solution_loader = IpoptSolutionLoader( sol_data=sol_data, nl_info=nl_info ) From 69fd8d03a5b2349c060a0b6d7a861d045681b4ec Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:23:16 -0700 Subject: [PATCH 0388/1178] Move PersistentUtils into own file --- pyomo/contrib/solver/persistent.py | 523 +++++++++++++++++++++++++++++ pyomo/contrib/solver/util.py | 512 +--------------------------- 2 files changed, 524 insertions(+), 511 deletions(-) create mode 100644 pyomo/contrib/solver/persistent.py diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py new file mode 100644 index 00000000000..0994aa53093 --- /dev/null +++ b/pyomo/contrib/solver/persistent.py @@ -0,0 +1,523 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# __________________________________________________________________________ + +import abc +from typing import List + +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective + + +class PersistentSolverUtils(abc.ABC): + def __init__(self): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + + def set_instance(self, model): + saved_config = self.config + self.__init__() + self.config = saved_config + self._model = model + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.config.auto_updates.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.config.auto_updates.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + self.add_constraints( + list( + block.component_data_objects(Constraint, descend_into=True, active=True) + ) + ) + self.add_sos_constraints( + list( + block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list( + block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ) + ) + self.remove_sos_constraints( + list( + block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.config.auto_updates + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if (fixed != v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.config.auto_updates.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.config.auto_updates.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.config.auto_updates.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index ca499748adf..c6bbfbd22ad 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -9,19 +9,9 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import abc -from typing import List - from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types import pyomo.core.expr as EXPR -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.objective import Objective, _GeneralObjectiveData -from pyomo.common.collections import ComponentMap -from pyomo.common.timing import HierarchicalTimer -from pyomo.core.expr.numvalue import NumericConstant +from pyomo.core.base.objective import Objective from pyomo.opt.results.solver import ( SolverStatus, TerminationCondition as LegacyTerminationCondition, @@ -151,503 +141,3 @@ def collect_vars_and_named_exprs(expr): list(_visitor.fixed_vars.values()), list(_visitor._external_functions.values()), ) - - -class PersistentSolverUtils(abc.ABC): - def __init__(self): - self._model = None - self._active_constraints = {} # maps constraint to (lower, body, upper) - self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) - self._params = {} # maps param id to param - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._named_expressions = ( - {} - ) # maps constraint to list of tuples (named_expr, named_expr.expr) - self._external_functions = ComponentMap() - self._obj_named_expressions = [] - self._referenced_variables = ( - {} - ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = {} - self._vars_referenced_by_obj = [] - self._expr_types = None - - def set_instance(self, model): - saved_config = self.config - self.__init__() - self.config = saved_config - self._model = model - self.add_block(model) - if self._objective is None: - self.set_objective(None) - - @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): - pass - - def add_variables(self, variables: List[_GeneralVarData]): - for v in variables: - if id(v) in self._referenced_variables: - raise ValueError( - 'variable {name} has already been added'.format(name=v.name) - ) - self._referenced_variables[id(v)] = [{}, {}, None] - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._add_variables(variables) - - @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): - pass - - def add_params(self, params: List[_ParamData]): - for p in params: - self._params[id(p)] = p - self._add_params(params) - - @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = {} - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - new_vars[v_id] = v - self.add_variables(list(new_vars.values())) - - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = {} - for v in variables: - v_id = id(v) - ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] - if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: - vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) - - def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = {} - for con in cons: - if con in self._named_expressions: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = (con.lower, con.body, con.upper) - tmp = collect_vars_and_named_exprs(con.body) - named_exprs, variables, fixed_vars, external_functions = tmp - self._check_for_new_vars(variables) - self._named_expressions[con] = [(e, e.expr) for e in named_exprs] - if len(external_functions) > 0: - self._external_functions[con] = external_functions - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][0][con] = None - if not self.config.auto_updates.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - all_fixed_vars[id(v)] = v - self._add_constraints(cons) - for v in all_fixed_vars.values(): - v.fix() - - @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def add_sos_constraints(self, cons: List[_SOSConstraintData]): - for con in cons: - if con in self._vars_referenced_by_con: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = tuple() - variables = con.get_variables() - self._check_for_new_vars(variables) - self._named_expressions[con] = [] - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][1][con] = None - self._add_sos_constraints(cons) - - @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): - pass - - def set_objective(self, obj: _GeneralObjectiveData): - if self._objective is not None: - for v in self._vars_referenced_by_obj: - self._referenced_variables[id(v)][2] = None - self._check_to_remove_vars(self._vars_referenced_by_obj) - self._external_functions.pop(self._objective, None) - if obj is not None: - self._objective = obj - self._objective_expr = obj.expr - self._objective_sense = obj.sense - tmp = collect_vars_and_named_exprs(obj.expr) - named_exprs, variables, fixed_vars, external_functions = tmp - self._check_for_new_vars(variables) - self._obj_named_expressions = [(i, i.expr) for i in named_exprs] - if len(external_functions) > 0: - self._external_functions[obj] = external_functions - self._vars_referenced_by_obj = variables - for v in variables: - self._referenced_variables[id(v)][2] = obj - if not self.config.auto_updates.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - self._set_objective(obj) - for v in fixed_vars: - v.fix() - else: - self._vars_referenced_by_obj = [] - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._obj_named_expressions = [] - self._set_objective(obj) - - def add_block(self, block): - param_dict = {} - for p in block.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) - self.add_constraints( - list( - block.component_data_objects(Constraint, descend_into=True, active=True) - ) - ) - self.add_sos_constraints( - list( - block.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - ) - ) - obj = get_objective(block) - if obj is not None: - self.set_objective(obj) - - @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def remove_constraints(self, cons: List[_GeneralConstraintData]): - self._remove_constraints(cons) - for con in cons: - if con not in self._named_expressions: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][0].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - self._external_functions.pop(con, None) - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): - self._remove_sos_constraints(cons) - for con in cons: - if con not in self._vars_referenced_by_con: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][1].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): - pass - - def remove_variables(self, variables: List[_GeneralVarData]): - self._remove_variables(variables) - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - raise ValueError( - 'cannot remove variable {name} - it has not been added'.format( - name=v.name - ) - ) - cons_using, sos_using, obj_using = self._referenced_variables[v_id] - if cons_using or sos_using or (obj_using is not None): - raise ValueError( - 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( - name=v.name - ) - ) - del self._referenced_variables[v_id] - del self._vars[v_id] - - @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): - pass - - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) - for p in params: - del self._params[id(p)] - - def remove_block(self, block): - self.remove_constraints( - list( - block.component_data_objects( - ctype=Constraint, descend_into=True, active=True - ) - ) - ) - self.remove_sos_constraints( - list( - block.component_data_objects( - ctype=SOSConstraint, descend_into=True, active=True - ) - ) - ) - self.remove_params( - list( - dict( - (id(p), p) - for p in block.component_data_objects( - ctype=Param, descend_into=True - ) - ).values() - ) - ) - - @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): - pass - - def update_variables(self, variables: List[_GeneralVarData]): - for v in variables: - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._update_variables(variables) - - @abc.abstractmethod - def update_params(self): - pass - - def update(self, timer: HierarchicalTimer = None): - if timer is None: - timer = HierarchicalTimer() - config = self.config.auto_updates - new_vars = [] - old_vars = [] - new_params = [] - old_params = [] - new_cons = [] - old_cons = [] - old_sos = [] - new_sos = [] - current_vars_dict = {} - current_cons_dict = {} - current_sos_dict = {} - timer.start('vars') - if config.update_vars: - start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - timer.stop('vars') - timer.start('params') - if config.check_for_new_or_removed_params: - current_params_dict = {} - for p in self._model.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - current_params_dict[id(_p)] = _p - for p_id, p in current_params_dict.items(): - if p_id not in self._params: - new_params.append(p) - for p_id, p in self._params.items(): - if p_id not in current_params_dict: - old_params.append(p) - timer.stop('params') - timer.start('cons') - if config.check_for_new_or_removed_constraints or config.update_constraints: - current_cons_dict = { - c: None - for c in self._model.component_data_objects( - Constraint, descend_into=True, active=True - ) - } - current_sos_dict = { - c: None - for c in self._model.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - } - for c in current_cons_dict.keys(): - if c not in self._vars_referenced_by_con: - new_cons.append(c) - for c in current_sos_dict.keys(): - if c not in self._vars_referenced_by_con: - new_sos.append(c) - for c in self._vars_referenced_by_con.keys(): - if c not in current_cons_dict and c not in current_sos_dict: - if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) - ): - old_cons.append(c) - else: - assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) - ) - old_sos.append(c) - self.remove_constraints(old_cons) - self.remove_sos_constraints(old_sos) - timer.stop('cons') - timer.start('params') - self.remove_params(old_params) - - # sticking this between removal and addition - # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() - - self.add_params(new_params) - timer.stop('params') - timer.start('vars') - self.add_variables(new_vars) - timer.stop('vars') - timer.start('cons') - self.add_constraints(new_cons) - self.add_sos_constraints(new_sos) - new_cons_set = set(new_cons) - new_sos_set = set(new_sos) - new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = {} - need_to_set_objective = False - if config.update_constraints: - cons_to_update = [] - sos_to_update = [] - for c in current_cons_dict.keys(): - if c not in new_cons_set: - cons_to_update.append(c) - for c in current_sos_dict.keys(): - if c not in new_sos_set: - sos_to_update.append(c) - for c in cons_to_update: - lower, body, upper = self._active_constraints[c] - new_lower, new_body, new_upper = c.lower, c.body, c.upper - if new_body is not body: - cons_to_remove_and_add[c] = None - continue - if new_lower is not lower: - if ( - type(new_lower) is NumericConstant - and type(lower) is NumericConstant - and new_lower.value == lower.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - if new_upper is not upper: - if ( - type(new_upper) is NumericConstant - and type(upper) is NumericConstant - and new_upper.value == upper.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - self.remove_sos_constraints(sos_to_update) - self.add_sos_constraints(sos_to_update) - timer.stop('cons') - timer.start('vars') - if config.update_vars: - end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] - if config.update_vars: - vars_to_update = [] - for v in vars_to_check: - _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if (fixed != v.fixed) or (fixed and (value != v.value)): - vars_to_update.append(v) - if self.config.auto_updates.treat_fixed_vars_as_params: - for c in self._referenced_variables[id(v)][0]: - cons_to_remove_and_add[c] = None - if self._referenced_variables[id(v)][2] is not None: - need_to_set_objective = True - elif lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif domain_interval != v.domain.get_interval(): - vars_to_update.append(v) - self.update_variables(vars_to_update) - timer.stop('vars') - timer.start('cons') - cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) - self.remove_constraints(cons_to_remove_and_add) - self.add_constraints(cons_to_remove_and_add) - timer.stop('cons') - timer.start('named expressions') - if config.update_named_expressions: - cons_to_update = [] - for c, expr_list in self._named_expressions.items(): - if c in new_cons_set: - continue - for named_expr, old_expr in expr_list: - if named_expr.expr is not old_expr: - cons_to_update.append(c) - break - self.remove_constraints(cons_to_update) - self.add_constraints(cons_to_update) - for named_expr, old_expr in self._obj_named_expressions: - if named_expr.expr is not old_expr: - need_to_set_objective = True - break - timer.stop('named expressions') - timer.start('objective') - if self.config.auto_updates.check_for_new_objective: - pyomo_obj = get_objective(self._model) - if pyomo_obj is not self._objective: - need_to_set_objective = True - else: - pyomo_obj = self._objective - if self.config.auto_updates.update_objective: - if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: - need_to_set_objective = True - elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: - # we can definitely do something faster here than resetting the whole objective - need_to_set_objective = True - if need_to_set_objective: - self.set_objective(pyomo_obj) - timer.stop('objective') - - # this has to be done after the objective and constraints in case the - # old objective/constraints use old variables - timer.start('vars') - self.remove_variables(old_vars) - timer.stop('vars') From 92c0262090702505ea0b35437713760fb4f78f57 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:23:59 -0700 Subject: [PATCH 0389/1178] Change import statement --- pyomo/contrib/solver/gurobi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 66e03a2a0f4..85131ba73bd 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -33,7 +33,7 @@ from pyomo.contrib.solver.base import PersistentSolverBase from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.contrib.solver.config import PersistentBranchAndBoundConfig -from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.persistent import PersistentSolverUtils from pyomo.contrib.solver.solution import PersistentSolutionLoader from pyomo.core.staleflag import StaleFlagManager import sys From 3447f0792f767590e806c5bb9929984c6ecb063f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:54:12 -0700 Subject: [PATCH 0390/1178] Update ipopt imports; change available and version --- .../developer_reference/solvers.rst | 12 ++--- pyomo/contrib/solver/ipopt.py | 45 ++++++++++--------- pyomo/contrib/solver/plugins.py | 4 +- .../solver/tests/solvers/test_ipopt.py | 4 +- .../solver/tests/solvers/test_solvers.py | 16 +++---- 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 8c8c9e5b8ee..921e452004d 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -63,7 +63,7 @@ or changed ``SolverFactory`` version. # Direct import import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination - from pyomo.contrib.solver.ipopt import ipopt + from pyomo.contrib.solver.ipopt import Ipopt model = pyo.ConcreteModel() model.x = pyo.Var(initialize=1.5) @@ -74,7 +74,7 @@ or changed ``SolverFactory`` version. model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - opt = ipopt() + opt = Ipopt() status = opt.solve(model) assert_optimal_termination(status) # Displays important results information; only available in future capability mode @@ -112,7 +112,7 @@ The new interface will allow for direct manipulation of linear presolve and scal options for certain solvers. Currently, these options are only available for ``ipopt``. -.. autoclass:: pyomo.contrib.solver.ipopt.ipopt +.. autoclass:: pyomo.contrib.solver.ipopt.Ipopt :members: solve The ``writer_config`` configuration option can be used to manipulate presolve @@ -120,8 +120,8 @@ and scaling options: .. code-block:: python - >>> from pyomo.contrib.solver.ipopt import ipopt - >>> opt = ipopt() + >>> from pyomo.contrib.solver.ipopt import Ipopt + >>> opt = Ipopt() >>> opt.config.writer_config.display() show_section_timing: false @@ -213,7 +213,7 @@ Solutions can be loaded back into a model using a ``SolutionLoader``. A specific loader should be written for each unique case. Several have already been implemented. For example, for ``ipopt``: -.. autoclass:: pyomo.contrib.solver.ipopt.ipoptSolutionLoader +.. autoclass:: pyomo.contrib.solver.ipopt.IpoptSolutionLoader :show-inheritance: :members: :inherited-members: diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 1aa00c0c8e2..3b1e95da42c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -221,20 +221,26 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None - self.executable = self.config.executable - - def available(self): - if self._available_cache is None: - if self.executable.path() is None: - self._available_cache = self.Availability.NotFound + self._executable = self.config.executable + + def available(self, config=None): + if config is None: + config = self.config + pth = config.executable.path() + if self._available_cache is None or self._available_cache[0] != pth: + if pth is None: + self._available_cache = (None, self.Availability.NotFound) else: - self._available_cache = self.Availability.FullLicense - return self._available_cache - - def version(self): - if self._version_cache is None: + self._available_cache = (pth, self.Availability.FullLicense) + return self._available_cache[1] + + def version(self, config=None): + if config is None: + config = self.config + pth = config.executable.path() + if self._version_cache is None or self._version_cache[0] != pth: results = subprocess.run( - [str(self.executable), '--version'], + [str(pth), '--version'], timeout=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -243,8 +249,8 @@ def version(self): version = results.stdout.splitlines()[0] version = version.split(' ')[1].strip() version = tuple(int(i) for i in version.split('.')) - self._version_cache = version - return self._version_cache + self._version_cache = (pth, version) + return self._version_cache[1] def _write_options_file(self, filename: str, options: Mapping): # First we need to determine if we even need to create a file. @@ -263,7 +269,7 @@ def _write_options_file(self, filename: str, options: Mapping): return opt_file_exists def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): - cmd = [str(self.executable), basename + '.nl', '-AMPL'] + cmd = [str(self._executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: @@ -285,15 +291,14 @@ def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: boo def solve(self, model, **kwds): # Begin time tracking start_timestamp = datetime.datetime.now(datetime.timezone.utc) + # Update configuration options, based on keywords passed to solve + config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) # Check if solver is available - avail = self.available() + avail = self.available(config) if not avail: raise ipoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) - # Update configuration options, based on keywords passed to solve - config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) - self.executable = config.executable if config.threads: logger.log( logging.WARNING, @@ -397,7 +402,7 @@ def solve(self, model, **kwds): ) results.solver_name = self.name - results.solver_version = self.version() + results.solver_version = self.version(config) if ( config.load_solutions and results.solution_status == SolutionStatus.noSolution diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index cb089200100..c7da41463a2 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -11,14 +11,14 @@ from .factory import SolverFactory -from .ipopt import ipopt +from .ipopt import Ipopt from .gurobi import Gurobi def load(): SolverFactory.register( name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)' - )(ipopt) + )(Ipopt) SolverFactory.register( name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' )(Gurobi) diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index 2886045055c..dc6bcf24855 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -13,7 +13,7 @@ import pyomo.environ as pyo from pyomo.common.fileutils import ExecutableData from pyomo.common.config import ConfigDict -from pyomo.contrib.solver.ipopt import ipoptConfig +from pyomo.contrib.solver.ipopt import IpoptConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.common import unittest @@ -42,7 +42,7 @@ def rosenbrock(m): def test_ipopt_config(self): # Test default initialization - config = ipoptConfig() + config = IpoptConfig() self.assertTrue(config.load_solutions) self.assertIsInstance(config.solver_options, ConfigDict) self.assertIsInstance(config.executable, ExecutableData) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index e5af2ada170..2b9e783ad16 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -17,7 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results from pyomo.contrib.solver.base import SolverBase -from pyomo.contrib.solver.ipopt import ipopt +from pyomo.contrib.solver.ipopt import Ipopt from pyomo.contrib.solver.gurobi import Gurobi from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression @@ -31,10 +31,10 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] +all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] mip_solvers = [('gurobi', Gurobi)] -nlp_solvers = [('ipopt', ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] +nlp_solvers = [('ipopt', Ipopt)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] miqcqp_solvers = [('gurobi', Gurobi)] @@ -256,7 +256,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') - if isinstance(opt, ipopt): + if isinstance(opt, Ipopt): opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() @@ -429,7 +429,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): opt.config.raise_exception_on_nonoptimal_result = False res = opt.solve(m) self.assertNotEqual(res.solution_status, SolutionStatus.optimal) - if isinstance(opt, ipopt): + if isinstance(opt, Ipopt): acceptable_termination_conditions = { TerminationCondition.locallyInfeasible, TerminationCondition.unbounded, @@ -444,7 +444,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, None) self.assertTrue(res.incumbent_objective is None) - if not isinstance(opt, ipopt): + if not isinstance(opt, Ipopt): # ipopt can return the values of the variables/duals at the last iterate # even if it did not converge; raise_exception_on_nonoptimal_result # is set to False, so we are free to load infeasible solutions @@ -970,7 +970,7 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase]): constant=0, ) m.c2[t] = expr == 1 - if isinstance(opt, ipopt): + if isinstance(opt, Ipopt): opt.config.time_limit = 1e-6 else: opt.config.time_limit = 0 From 74a9c7b32a44c9242d8f1abdb56045e8ae99bcb8 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 11:57:03 -0700 Subject: [PATCH 0391/1178] add mpc readme --- pyomo/contrib/mpc/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pyomo/contrib/mpc/README.md diff --git a/pyomo/contrib/mpc/README.md b/pyomo/contrib/mpc/README.md new file mode 100644 index 00000000000..21fe39c5f50 --- /dev/null +++ b/pyomo/contrib/mpc/README.md @@ -0,0 +1,34 @@ +# Pyomo MPC + +Pyomo MPC is an extension for developing model predictive control simulations +using Pyomo models. Please see the +[documentation](https://pyomo.readthedocs.io/en/stable/contributed_packages/mpc/index.html) +for more detailed information. + +Pyomo MPC helps with, among other things, the following use cases: +- Transfering values between different points in time in a dynamic model +(e.g. to initialize a dynamic model to its initial conditions) +- Extracting or loading disturbances and inputs from or to models, and storing +these in model-agnostic, easily JSON-serializable data structures +- Constructing common modeling components, such as weighted-least-squares +tracking objective functions, piecewise-constant input constraints, or +terminal region constraints. + +## Citation + +If you use Pyomo MPC in your research, please cite the following paper, which +discusses the motivation for the Pyomo MPC data structures and the underlying +Pyomo features that make them possible. +```bibtex +@article{parker2023mpc, +title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, +journal = {Journal of Process Control}, +volume = {132}, +pages = {103113}, +year = {2023}, +issn = {0959-1524}, +doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, +url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, +author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, +} +``` From 321a18f8085d5b0c77ed6295606bd0b4d221e0ae Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 12:04:13 -0700 Subject: [PATCH 0392/1178] add citation to mpc/index --- .../contributed_packages/mpc/index.rst | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/mpc/index.rst b/doc/OnlineDocs/contributed_packages/mpc/index.rst index b93abf223e2..c9ac929a71a 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/index.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/index.rst @@ -1,7 +1,7 @@ MPC === -This package contains data structures and utilities for dynamic optimization +Pyomo MPC contains data structures and utilities for dynamic optimization and rolling horizon applications, e.g. model predictive control. .. toctree:: @@ -10,3 +10,22 @@ and rolling horizon applications, e.g. model predictive control. overview.rst examples.rst faq.rst + +Citation +-------- + +If you use Pyomo MPC in your research, please cite the following paper: + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } From b902a30ec8dd0f259aaef32516b3d94009ddb448 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 12:16:42 -0700 Subject: [PATCH 0393/1178] If sol file exists, parse it --- pyomo/contrib/solver/ipopt.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 3b1e95da42c..580f350dff3 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -380,16 +380,18 @@ def solve(self, model, **kwds): ostreams[0] ) - if process.returncode != 0: + if os.path.isfile(basename + '.sol'): + with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') + results = self._parse_solution(sol_file, nl_info) + timer.stop('parse_sol') + else: results = IpoptResults() + if process.returncode != 0: results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error results.solution_loader = SolSolutionLoader(None, None) else: - with open(basename + '.sol', 'r') as sol_file: - timer.start('parse_sol') - results = self._parse_solution(sol_file, nl_info) - timer.stop('parse_sol') results.iteration_count = iters results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc results.timing_info.nlp_function_evaluations = ipopt_time_func From 15eddd3a21560cd46d306cee15fba3a61863dedc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 12:37:13 -0700 Subject: [PATCH 0394/1178] Update _parse_ipopt_output to address newer versions of IPOPT --- pyomo/contrib/solver/ipopt.py | 46 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 580f350dff3..fc009c77522 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -101,14 +101,33 @@ def __init__( ) self.timing_info.ipopt_excluding_nlp_functions: Optional[float] = ( self.timing_info.declare( - 'ipopt_excluding_nlp_functions', ConfigValue(domain=NonNegativeFloat) + 'ipopt_excluding_nlp_functions', + ConfigValue( + domain=NonNegativeFloat, + default=None, + description="Total CPU seconds in IPOPT without function evaluations.", + ), ) ) self.timing_info.nlp_function_evaluations: Optional[float] = ( self.timing_info.declare( - 'nlp_function_evaluations', ConfigValue(domain=NonNegativeFloat) + 'nlp_function_evaluations', + ConfigValue( + domain=NonNegativeFloat, + default=None, + description="Total CPU seconds in NLP function evaluations.", + ), ) ) + self.timing_info.total_seconds: Optional[float] = self.timing_info.declare( + 'total_seconds', + ConfigValue( + domain=NonNegativeFloat, + default=None, + description="Total seconds in IPOPT. NOTE: Newer versions of IPOPT (3.14+) " + "no longer separate timing information.", + ), + ) class IpoptSolutionLoader(SolSolutionLoader): @@ -376,8 +395,8 @@ def solve(self, model, **kwds): timer.stop('subprocess') # This is the stuff we need to parse to get the iterations # and time - iters, ipopt_time_nofunc, ipopt_time_func = self._parse_ipopt_output( - ostreams[0] + iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( + self._parse_ipopt_output(ostreams[0]) ) if os.path.isfile(basename + '.sol'): @@ -395,12 +414,14 @@ def solve(self, model, **kwds): results.iteration_count = iters results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc results.timing_info.nlp_function_evaluations = ipopt_time_func + results.timing_info.total_seconds = ipopt_total_time if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal ): raise RuntimeError( - 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + 'Solver did not find the optimal solution. Set ' + 'opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' ) results.solver_name = self.name @@ -411,7 +432,7 @@ def solve(self, model, **kwds): ): raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' - 'Please set config.load_solutions=False to bypass this error.' + 'Please set opt.config.load_solutions=False to bypass this error.' ) if config.load_solutions: @@ -472,23 +493,30 @@ def _parse_ipopt_output(self, stream: io.StringIO): iters = None nofunc_time = None func_time = None + total_time = None # parse the output stream to get the iteration count and solver time for line in stream.getvalue().splitlines(): if line.startswith("Number of Iterations....:"): tokens = line.split() iters = int(tokens[3]) + elif line.startswith( + "Total seconds in IPOPT =" + ): + # Newer versions of IPOPT no longer separate the + tokens = line.split() + total_time = float(tokens[-1]) elif line.startswith( "Total CPU secs in IPOPT (w/o function evaluations) =" ): tokens = line.split() - nofunc_time = float(tokens[9]) + nofunc_time = float(tokens[-1]) elif line.startswith( "Total CPU secs in NLP function evaluations =" ): tokens = line.split() - func_time = float(tokens[8]) + func_time = float(tokens[-1]) - return iters, nofunc_time, func_time + return iters, nofunc_time, func_time, total_time def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): results = IpoptResults() From d8536b6cd1751c594d81c7c88ca0bcaa0d380589 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 12:37:26 -0700 Subject: [PATCH 0395/1178] add api docs to mpi --- doc/OnlineDocs/contributed_packages/mpc/api.rst | 10 ++++++++++ .../contributed_packages/mpc/conversion.rst | 5 +++++ .../contributed_packages/mpc/data.rst | 17 +++++++++++++++++ .../contributed_packages/mpc/index.rst | 1 + .../contributed_packages/mpc/interface.rst | 8 ++++++++ .../contributed_packages/mpc/modeling.rst | 11 +++++++++++ 6 files changed, 52 insertions(+) create mode 100644 doc/OnlineDocs/contributed_packages/mpc/api.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/conversion.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/data.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/interface.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/modeling.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/api.rst b/doc/OnlineDocs/contributed_packages/mpc/api.rst new file mode 100644 index 00000000000..2752fea8af6 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/api.rst @@ -0,0 +1,10 @@ +.. _mpc_api: + +API Reference +============= + +.. toctree:: + data.rst + conversion.rst + interface.rst + modeling.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/conversion.rst b/doc/OnlineDocs/contributed_packages/mpc/conversion.rst new file mode 100644 index 00000000000..9d9406edb75 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/conversion.rst @@ -0,0 +1,5 @@ +Data Conversion +=============== + +.. automodule:: pyomo.contrib.mpc.data.convert + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/data.rst b/doc/OnlineDocs/contributed_packages/mpc/data.rst new file mode 100644 index 00000000000..73cb6543b1e --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/data.rst @@ -0,0 +1,17 @@ +Data Structures +=============== + +.. automodule:: pyomo.contrib.mpc.data.get_cuid + :members: + +.. automodule:: pyomo.contrib.mpc.data.dynamic_data_base + :members: + +.. automodule:: pyomo.contrib.mpc.data.scalar_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.series_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.interval_data + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/index.rst b/doc/OnlineDocs/contributed_packages/mpc/index.rst index c9ac929a71a..e512d1a6ef5 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/index.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/index.rst @@ -10,6 +10,7 @@ and rolling horizon applications, e.g. model predictive control. overview.rst examples.rst faq.rst + api.rst Citation -------- diff --git a/doc/OnlineDocs/contributed_packages/mpc/interface.rst b/doc/OnlineDocs/contributed_packages/mpc/interface.rst new file mode 100644 index 00000000000..eb5bac548fd --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/interface.rst @@ -0,0 +1,8 @@ +Interfaces +========== + +.. automodule:: pyomo.contrib.mpc.interfaces.model_interface + :members: + +.. automodule:: pyomo.contrib.mpc.interfaces.var_linker + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/modeling.rst b/doc/OnlineDocs/contributed_packages/mpc/modeling.rst new file mode 100644 index 00000000000..cbae03161b1 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/modeling.rst @@ -0,0 +1,11 @@ +Modeling Components +=================== + +.. automodule:: pyomo.contrib.mpc.modeling.constraints + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.cost_expressions + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.terminal + :members: From 7ae3ed8d737f82857ab8e5654bb1c7f6973933c9 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 12:53:13 -0700 Subject: [PATCH 0396/1178] clarify definition of "flatten" and add citation --- .../advanced_topics/flattener/index.rst | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/advanced_topics/flattener/index.rst b/doc/OnlineDocs/advanced_topics/flattener/index.rst index 377de5233ec..982a8931d36 100644 --- a/doc/OnlineDocs/advanced_topics/flattener/index.rst +++ b/doc/OnlineDocs/advanced_topics/flattener/index.rst @@ -30,8 +30,9 @@ The ``pyomo.dae.flatten`` module aims to address this use case by providing utilities to generate all components indexed, explicitly or implicitly, by user-provided sets. -**When we say "flatten a model," we mean "generate all components in the model, -preserving all user-specified indexing sets."** +**When we say "flatten a model," we mean "recursively generate all components in +the model, where a component can be indexed only by user-specified indexing +sets (or is not indexed at all)**. Data structures --------------- @@ -42,3 +43,23 @@ Slices are necessary as they can encode "implicit indexing" -- where a component is contained in an indexed block. It is natural to return references to these slices, so they may be accessed and manipulated like any other component. + +Citation +-------- +If you use the ``pyomo.dae.flatten`` module in your research, we would appreciate +you citing the following paper, which gives more detail about the motivation for +and examples of using this functinoality. + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } From 90d03c15b3e732b2f00f5f49ab6f63d6e1f3f744 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 13:03:36 -0700 Subject: [PATCH 0397/1178] improve docstring and fix typo --- pyomo/contrib/mpc/data/get_cuid.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mpc/data/get_cuid.py b/pyomo/contrib/mpc/data/get_cuid.py index 03659d6153f..ef0df7ea679 100644 --- a/pyomo/contrib/mpc/data/get_cuid.py +++ b/pyomo/contrib/mpc/data/get_cuid.py @@ -16,14 +16,13 @@ def get_indexed_cuid(var, sets=None, dereference=None, context=None): - """ - Attempts to convert the provided "var" object into a CUID with - with wildcards. + """Attempt to convert the provided "var" object into a CUID with wildcards Arguments --------- var: - Object to process + Object to process. May be a VarData, IndexedVar (reference or otherwise), + ComponentUID, slice, or string. sets: Tuple of sets Sets to use if slicing a vardata object dereference: None or int @@ -32,6 +31,11 @@ def get_indexed_cuid(var, sets=None, dereference=None, context=None): context: Block Block with respect to which slices and CUIDs will be generated + Returns + ------- + ``ComponentUID`` + ComponentUID corresponding to the provided ``var`` and sets + """ # TODO: Does this function have a good name? # Should this function be generalized beyond a single indexing set? From bc4c71bf3469ac1fa68f888290bfc7f42458f7a7 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 13:07:08 -0700 Subject: [PATCH 0398/1178] add end quote --- doc/OnlineDocs/advanced_topics/flattener/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/advanced_topics/flattener/index.rst b/doc/OnlineDocs/advanced_topics/flattener/index.rst index 982a8931d36..f9dd8ea6abb 100644 --- a/doc/OnlineDocs/advanced_topics/flattener/index.rst +++ b/doc/OnlineDocs/advanced_topics/flattener/index.rst @@ -31,7 +31,7 @@ utilities to generate all components indexed, explicitly or implicitly, by user-provided sets. **When we say "flatten a model," we mean "recursively generate all components in -the model, where a component can be indexed only by user-specified indexing +the model," where a component can be indexed only by user-specified indexing sets (or is not indexed at all)**. Data structures From 76c970f35f9cedfa6890b64fddd30570f53961a4 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 13:08:03 -0700 Subject: [PATCH 0399/1178] fix typo --- pyomo/contrib/mpc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mpc/README.md b/pyomo/contrib/mpc/README.md index 21fe39c5f50..7e03163f703 100644 --- a/pyomo/contrib/mpc/README.md +++ b/pyomo/contrib/mpc/README.md @@ -6,7 +6,7 @@ using Pyomo models. Please see the for more detailed information. Pyomo MPC helps with, among other things, the following use cases: -- Transfering values between different points in time in a dynamic model +- Transferring values between different points in time in a dynamic model (e.g. to initialize a dynamic model to its initial conditions) - Extracting or loading disturbances and inputs from or to models, and storing these in model-agnostic, easily JSON-serializable data structures From afcedb15449f76d308d73a96fed18dbb0eda5638 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 13:27:08 -0700 Subject: [PATCH 0400/1178] Incomplete comment; stronger parsing --- pyomo/contrib/solver/ipopt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index fc009c77522..074e5b19c5c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -498,11 +498,13 @@ def _parse_ipopt_output(self, stream: io.StringIO): for line in stream.getvalue().splitlines(): if line.startswith("Number of Iterations....:"): tokens = line.split() - iters = int(tokens[3]) + iters = int(tokens[-1]) elif line.startswith( "Total seconds in IPOPT =" ): - # Newer versions of IPOPT no longer separate the + # Newer versions of IPOPT no longer separate timing into + # two different values. This is so we have compatibility with + # both new and old versions tokens = line.split() total_time = float(tokens[-1]) elif line.startswith( From dfea3ecca321cef955e31d9a5d48412014f2fad6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 13:29:51 -0700 Subject: [PATCH 0401/1178] Update Component{Map,Set} to support tuple keys --- pyomo/common/collections/component_map.py | 56 +++++++++++++----- pyomo/common/collections/component_set.py | 70 +++++++++++------------ 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 80ba5fe0d1c..0851ffad301 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -9,21 +9,49 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableMapping as collections_MutableMapping +import collections from collections.abc import Mapping as collections_Mapping from pyomo.common.autoslots import AutoSlots -def _rebuild_ids(encode, val): +def _rehash_keys(encode, val): if encode: - return val + return list(val.values()) else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {id(obj): (obj, v) for obj, v in val.values()} + return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val} + + +class _Hasher(collections.defaultdict): + def __init__(self, *args, **kwargs): + super().__init__(lambda: self._missing_impl, *args, **kwargs) + self[tuple] = self._tuple + + def _missing_impl(self, val): + try: + hash(val) + self[val.__class__] = self._hashable + except: + self[val.__class__] = self._unhashable + return self[val.__class__](val) + + @staticmethod + def _hashable(val): + return val + + @staticmethod + def _unhashable(val): + return id(val) + + def _tuple(self, val): + return tuple(self[i.__class__](i) for i in val) + + +_hasher = _Hasher() -class ComponentMap(AutoSlots.Mixin, collections_MutableMapping): +class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): """ This class is a replacement for dict that allows Pyomo modeling components to be used as entry keys. The @@ -49,7 +77,7 @@ class ComponentMap(AutoSlots.Mixin, collections_MutableMapping): """ __slots__ = ("_dict",) - __autoslot_mappers__ = {'_dict': _rebuild_ids} + __autoslot_mappers__ = {'_dict': _rehash_keys} def __init__(self, *args, **kwds): # maps id(obj) -> (obj,val) @@ -68,18 +96,20 @@ def __str__(self): def __getitem__(self, obj): try: - return self._dict[id(obj)][1] + return self._dict[_hasher[obj.__class__](obj)][1] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(obj), str(obj))) + _id = _hasher[obj.__class__](obj) + raise KeyError("Component with id '%s': %s" % (_id, obj)) def __setitem__(self, obj, val): - self._dict[id(obj)] = (obj, val) + self._dict[_hasher[obj.__class__](obj)] = (obj, val) def __delitem__(self, obj): try: - del self._dict[id(obj)] + del self._dict[_hasher[obj.__class__](obj)] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(obj), str(obj))) + _id = _hasher[obj.__class__](obj) + raise KeyError("Component with id '%s': %s" % (_id, obj)) def __iter__(self): return (obj for obj, val in self._dict.values()) @@ -107,7 +137,7 @@ def __eq__(self, other): return False # Note we have already verified the dicts are the same size for key, val in other.items(): - other_id = id(key) + other_id = _hasher[key.__class__](key) if other_id not in self._dict: return False self_val = self._dict[other_id][1] @@ -130,7 +160,7 @@ def __ne__(self, other): # def __contains__(self, obj): - return id(obj) in self._dict + return _hasher[obj.__class__](obj) in self._dict def clear(self): 'D.clear() -> None. Remove all items from D.' diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index dfeac5cbfa5..f1fe7bc8cd6 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -12,8 +12,20 @@ from collections.abc import MutableSet as collections_MutableSet from collections.abc import Set as collections_Set +from pyomo.common.autoslots import AutoSlots +from pyomo.common.collections.component_map import _hasher -class ComponentSet(collections_MutableSet): + +def _rehash_keys(encode, val): + if encode: + return list(val.values()) + else: + # object id() may have changed after unpickling, + # so we rebuild the dictionary keys + return {_hasher[obj.__class__](obj): obj for obj in val} + + +class ComponentSet(AutoSlots.Mixin, collections_MutableSet): """ This class is a replacement for set that allows Pyomo modeling components to be used as entries. The @@ -38,16 +50,12 @@ class ComponentSet(collections_MutableSet): """ __slots__ = ("_data",) + __autoslot_mappers__ = {'_data': _rehash_keys} - def __init__(self, *args): - self._data = dict() - if len(args) > 0: - if len(args) > 1: - raise TypeError( - "%s expected at most 1 arguments, " - "got %s" % (self.__class__.__name__, len(args)) - ) - self.update(args[0]) + def __init__(self, iterable=None): + self._data = {} + if iterable is not None: + self.update(iterable) def __str__(self): """String representation of the mapping.""" @@ -56,29 +64,19 @@ def __str__(self): tmp.append(str(obj) + " (id=" + str(objid) + ")") return "ComponentSet(" + str(tmp) + ")" - def update(self, args): + def update(self, iterable): """Update a set with the union of itself and others.""" - self._data.update((id(obj), obj) for obj in args) - - # - # This method must be defined for deepcopy/pickling - # because this class relies on Python ids. - # - def __setstate__(self, state): - # object id() may have changed after unpickling, - # so we rebuild the dictionary keys - assert len(state) == 1 - self._data = {id(obj): obj for obj in state['_data']} - - def __getstate__(self): - return {'_data': tuple(self._data.values())} + if isinstance(iterable, ComponentSet): + self._data.update(iterable._data) + else: + self._data.update((_hasher[val.__class__](val), val) for val in iterable) # # Implement MutableSet abstract methods # def __contains__(self, val): - return self._data.__contains__(id(val)) + return _hasher[val.__class__](val) in self._data def __iter__(self): return iter(self._data.values()) @@ -88,27 +86,26 @@ def __len__(self): def add(self, val): """Add an element.""" - self._data[id(val)] = val + self._data[_hasher[val.__class__](val)] = val def discard(self, val): """Remove an element. Do not raise an exception if absent.""" - if id(val) in self._data: - del self._data[id(val)] + _id = _hasher[val.__class__](val) + if _id in self._data: + del self._data[_id] # # Overload MutableSet default implementations # - # We want to avoid generating Pyomo expressions due to - # comparison of values, so we convert both objects to a - # plain dictionary mapping key->(type(val), id(val)) and - # compare that instead. def __eq__(self, other): if self is other: return True if not isinstance(other, collections_Set): return False - return len(self) == len(other) and all(id(key) in self._data for key in other) + return len(self) == len(other) and all( + _hasher[val.__class__](val) in self._data for val in other + ) def __ne__(self, other): return not (self == other) @@ -125,6 +122,7 @@ def clear(self): def remove(self, val): """Remove an element. If not a member, raise a KeyError.""" try: - del self._data[id(val)] + del self._data[_hasher[val.__class__](val)] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(val), str(val))) + _id = _hasher[val.__class__](val) + raise KeyError("Component with id '%s': %s" % (_id, val)) From b817cfcb1362e59cef492329aa835a5e4cdad2d7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 13:30:18 -0700 Subject: [PATCH 0402/1178] Add test if using tuples in ComponentMap --- pyomo/common/tests/test_component_map.py | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 pyomo/common/tests/test_component_map.py diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py new file mode 100644 index 00000000000..cc746642f28 --- /dev/null +++ b/pyomo/common/tests/test_component_map.py @@ -0,0 +1,50 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.common.collections import ComponentMap +from pyomo.environ import ConcreteModel, Var, Constraint + + +class TestComponentMap(unittest.TestCase): + def test_tuple(self): + m = ConcreteModel() + m.v = Var() + m.c = Constraint(expr=m.v >= 0) + m.cm = cm = ComponentMap() + + cm[(1,2)] = 5 + self.assertEqual(len(cm), 1) + self.assertIn((1,2), cm) + self.assertEqual(cm[1,2], 5) + + cm[(1,2)] = 50 + self.assertEqual(len(cm), 1) + self.assertIn((1,2), cm) + self.assertEqual(cm[1,2], 50) + + cm[(1, (2, m.v))] = 10 + self.assertEqual(len(cm), 2) + self.assertIn((1,(2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 10) + + cm[(1, (2, m.v))] = 100 + self.assertEqual(len(cm), 2) + self.assertIn((1,(2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 100) + + i = m.clone() + self.assertIn((1, 2), i.cm) + self.assertIn((1, (2, i.v)), i.cm) + self.assertNotIn((1, (2, i.v)), m.cm) + self.assertIn((1, (2, m.v)), m.cm) + self.assertNotIn((1, (2, m.v)), i.cm) From eb672d5e0190ea9c27d40e66e252b07f37b06353 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 13:32:42 -0700 Subject: [PATCH 0403/1178] Clean up / update OrderedSet (for post-Python 3.7) --- pyomo/common/collections/orderedset.py | 30 ++++++++------------------ 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index f29245b75fe..6bcf0c2fafb 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -9,42 +9,30 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableSet from collections import OrderedDict +from collections.abc import MutableSet +from pyomo.common.autoslots import AutoSlots -class OrderedSet(MutableSet): +class OrderedSet(AutoSlots.Mixin, MutableSet): __slots__ = ('_dict',) def __init__(self, iterable=None): # TODO: Starting in Python 3.7, dict is ordered (and is faster # than OrderedDict). dict began supporting reversed() in 3.8. - # We should consider changing the underlying data type here from - # OrderedDict to dict. - self._dict = OrderedDict() + self._dict = {} if iterable is not None: - if iterable.__class__ is OrderedSet: - self._dict.update(iterable._dict) - else: - self.update(iterable) + self.update(iterable) def __str__(self): """String representation of the mapping.""" return "OrderedSet(%s)" % (', '.join(repr(x) for x in self)) def update(self, iterable): - for val in iterable: - self.add(val) - - # - # This method must be defined for deepcopy/pickling - # because this class is slotized. - # - def __setstate__(self, state): - self._dict = state - - def __getstate__(self): - return self._dict + if isinstance(iterable, OrderedSet): + self._dict.update(iterable._dict) + else: + self._dict.update((val, None) for val in iterable) # # Implement MutableSet abstract methods From d91ced077a8c3a7beb2f8cf7785e0e8e0f532a22 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Feb 2024 15:38:40 -0500 Subject: [PATCH 0404/1178] Simplify `PathList.__call__` --- pyomo/common/config.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index f156bee79a9..09a1706ee5a 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -589,15 +589,11 @@ class PathList(Path): """ def __call__(self, data): - try: - pathlist = [super(PathList, self).__call__(data)] - except TypeError as err: - is_not_path_like = "expected str, bytes or os.PathLike" in str(err) - if is_not_path_like and hasattr(data, "__iter__"): - pathlist = [super(PathList, self).__call__(i) for i in data] - else: - raise - return pathlist + is_path_like = isinstance(data, (str, bytes)) or hasattr(data, "__fspath__") + if hasattr(data, "__iter__") and not is_path_like: + return [super(PathList, self).__call__(i) for i in data] + else: + return [super(PathList, self).__call__(data)] class DynamicImplicitDomain(object): From 4c0effdbf74263f9cb9cf6cba6708d537194775f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:00:23 -0700 Subject: [PATCH 0405/1178] Add overwrite flag --- .github/workflows/release_wheel_creation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index ef44806d6d4..f978415e99e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -45,6 +45,7 @@ jobs: with: name: native_wheels path: dist/*.whl + overwrite: true alternative_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 @@ -76,6 +77,7 @@ jobs: with: name: alt_wheels path: dist/*.whl + overwrite: true generictarball: name: ${{ matrix.TARGET }} @@ -106,4 +108,5 @@ jobs: with: name: generictarball path: dist + overwrite: true From 153e24920cfa77ffe418dfba3a77178c900dc8b3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:07:05 -0700 Subject: [PATCH 0406/1178] Update action version --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index f978415e99e..19c2a6c50a9 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2 with: output-dir: dist env: @@ -63,7 +63,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2 with: output-dir: dist env: From fcb7cef0f64fe3457f3b2e955bb41ba5c8831189 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:11:56 -0700 Subject: [PATCH 0407/1178] Update action version --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 19c2a6c50a9..2dd44652489 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build wheels - uses: pypa/cibuildwheel@v2 + uses: pypa/cibuildwheel@v2.16.5 with: output-dir: dist env: @@ -63,7 +63,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2 + uses: pypa/cibuildwheel@v2.16.5 with: output-dir: dist env: From 217adeaec63873ec6ce945a240cd70d087587d98 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 14:15:35 -0700 Subject: [PATCH 0408/1178] NFC: apply black --- pyomo/common/tests/test_component_map.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py index cc746642f28..9e771175d42 100644 --- a/pyomo/common/tests/test_component_map.py +++ b/pyomo/common/tests/test_component_map.py @@ -22,24 +22,24 @@ def test_tuple(self): m.c = Constraint(expr=m.v >= 0) m.cm = cm = ComponentMap() - cm[(1,2)] = 5 + cm[(1, 2)] = 5 self.assertEqual(len(cm), 1) - self.assertIn((1,2), cm) - self.assertEqual(cm[1,2], 5) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 5) - cm[(1,2)] = 50 + cm[(1, 2)] = 50 self.assertEqual(len(cm), 1) - self.assertIn((1,2), cm) - self.assertEqual(cm[1,2], 50) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 50) cm[(1, (2, m.v))] = 10 self.assertEqual(len(cm), 2) - self.assertIn((1,(2, m.v)), cm) + self.assertIn((1, (2, m.v)), cm) self.assertEqual(cm[1, (2, m.v)], 10) cm[(1, (2, m.v))] = 100 self.assertEqual(len(cm), 2) - self.assertIn((1,(2, m.v)), cm) + self.assertIn((1, (2, m.v)), cm) self.assertEqual(cm[1, (2, m.v)], 100) i = m.clone() From a80c20605e61f764ec2371683676ace2739f0a96 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:18:19 -0700 Subject: [PATCH 0409/1178] Change from overwrite to merge --- .github/workflows/release_wheel_creation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 2dd44652489..203b4f391c7 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -45,7 +45,7 @@ jobs: with: name: native_wheels path: dist/*.whl - overwrite: true + merge-multiple: true alternative_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 @@ -77,7 +77,7 @@ jobs: with: name: alt_wheels path: dist/*.whl - overwrite: true + merge-multiple: true generictarball: name: ${{ matrix.TARGET }} @@ -108,5 +108,5 @@ jobs: with: name: generictarball path: dist - overwrite: true + merge-multiple: true From 88aab73d33d8cfaecef8337a3121f986285f6a2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:24:49 -0700 Subject: [PATCH 0410/1178] Have to give unique names now. Yay. --- .github/workflows/release_wheel_creation.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 203b4f391c7..d847b0f2cff 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,9 +43,9 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: native_wheels + name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} path: dist/*.whl - merge-multiple: true + overwrite: true alternative_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 @@ -75,9 +75,9 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels + name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} path: dist/*.whl - merge-multiple: true + overwrite: true generictarball: name: ${{ matrix.TARGET }} @@ -108,5 +108,5 @@ jobs: with: name: generictarball path: dist - merge-multiple: true + overwrite: true From dd9cf09b290302e854eb032644cf9e0ed8ee9509 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:35:52 -0700 Subject: [PATCH 0411/1178] Add fail fast; target names --- .github/workflows/release_wheel_creation.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index d847b0f2cff..6b65938706c 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -22,10 +22,23 @@ jobs: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture runs-on: ${{ matrix.os }} strategy: + fail-fast: true matrix: os: [ubuntu-22.04, windows-latest, macos-latest] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] + + include: + - wheel-version: 'cp38*' + TARGET: 'py38' + - wheel-version: 'cp39*' + TARGET: 'py39' + - wheel-version: 'cp310*' + TARGET: 'py310' + - wheel-version: 'cp311*' + TARGET: 'py311' + - wheel-version: 'cp312*' + TARGET: 'py312' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -43,7 +56,7 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} + name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} path: dist/*.whl overwrite: true @@ -75,7 +88,7 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} + name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} path: dist/*.whl overwrite: true From 8c643ca08867b646a7dea8964ff20061737ee1fb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:57:34 -0700 Subject: [PATCH 0412/1178] Copy-pasta failure --- .github/workflows/release_wheel_creation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 6b65938706c..17152dc3d1e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -56,7 +56,7 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} + name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} path: dist/*.whl overwrite: true From 5ad721cc20d8e2db73df7beb19d029cf1d11e46b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 15:27:24 -0700 Subject: [PATCH 0413/1178] updating results processing and tests --- pyomo/contrib/solver/ipopt.py | 147 +++++--- pyomo/contrib/solver/results.py | 4 + pyomo/contrib/solver/solution.py | 64 +++- .../solver/tests/solvers/test_solvers.py | 350 +++++++++++++++--- 4 files changed, 439 insertions(+), 126 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index fc009c77522..82d145d2e93 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -134,10 +134,20 @@ class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if len(self._nl_info.eliminated_vars) > 0: + raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.') + assert self._sol_data is not None if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) + obj_scale = 1 else: scale_list = self._nl_info.scaling.variables + obj_scale = self._nl_info.scaling.objectives[0] sol_data = self._sol_data nl_info = self._nl_info zl_map = sol_data.var_suffixes['ipopt_zL_out'] @@ -148,11 +158,11 @@ def get_reduced_costs( v_id = id(v) rc[v_id] = (v, 0) if ndx in zl_map: - zl = zl_map[ndx] * scale + zl = zl_map[ndx] * scale / obj_scale if abs(zl) > abs(rc[v_id][1]): rc[v_id] = (v, zl) if ndx in zu_map: - zu = zu_map[ndx] * scale + zu = zu_map[ndx] * scale / obj_scale if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) @@ -353,68 +363,82 @@ def solve(self, model, **kwds): symbolic_solver_labels=config.symbolic_solver_labels, ) timer.stop('write_nl_file') - # Get a copy of the environment to pass to the subprocess - env = os.environ.copy() - if nl_info.external_function_libraries: - if env.get('AMPLFUNC'): - nl_info.external_function_libraries.append(env.get('AMPLFUNC')) - env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) - # Write the opt_file, if there should be one; return a bool to say - # whether or not we have one (so we can correctly build the command line) - opt_file = self._write_options_file( - filename=basename, options=config.solver_options - ) - # Call ipopt - passing the files via the subprocess - cmd = self._create_command_line( - basename=basename, config=config, opt_file=opt_file - ) - # this seems silly, but we have to give the subprocess slightly longer to finish than - # ipopt - if config.time_limit is not None: - timeout = config.time_limit + min( - max(1.0, 0.01 * config.time_limit), 100 + if len(nl_info.variables) > 0: + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if nl_info.external_function_libraries: + if env.get('AMPLFUNC'): + nl_info.external_function_libraries.append(env.get('AMPLFUNC')) + env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) + # Write the opt_file, if there should be one; return a bool to say + # whether or not we have one (so we can correctly build the command line) + opt_file = self._write_options_file( + filename=basename, options=config.solver_options ) - else: - timeout = None - - ostreams = [io.StringIO()] - if config.tee: - ostreams.append(sys.stdout) - if config.log_solver_output: - ostreams.append(LogStream(level=logging.INFO, logger=logger)) - with TeeStream(*ostreams) as t: - timer.start('subprocess') - process = subprocess.run( - cmd, - timeout=timeout, - env=env, - universal_newlines=True, - stdout=t.STDOUT, - stderr=t.STDERR, - ) - timer.stop('subprocess') - # This is the stuff we need to parse to get the iterations - # and time - iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( - self._parse_ipopt_output(ostreams[0]) + # Call ipopt - passing the files via the subprocess + cmd = self._create_command_line( + basename=basename, config=config, opt_file=opt_file ) + # this seems silly, but we have to give the subprocess slightly longer to finish than + # ipopt + if config.time_limit is not None: + timeout = config.time_limit + min( + max(1.0, 0.01 * config.time_limit), 100 + ) + else: + timeout = None + + ostreams = [io.StringIO()] + if config.tee: + ostreams.append(sys.stdout) + if config.log_solver_output: + ostreams.append(LogStream(level=logging.INFO, logger=logger)) + with TeeStream(*ostreams) as t: + timer.start('subprocess') + process = subprocess.run( + cmd, + timeout=timeout, + env=env, + universal_newlines=True, + stdout=t.STDOUT, + stderr=t.STDERR, + ) + timer.stop('subprocess') + # This is the stuff we need to parse to get the iterations + # and time + iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( + self._parse_ipopt_output(ostreams[0]) + ) - if os.path.isfile(basename + '.sol'): - with open(basename + '.sol', 'r') as sol_file: - timer.start('parse_sol') - results = self._parse_solution(sol_file, nl_info) - timer.stop('parse_sol') - else: - results = IpoptResults() - if process.returncode != 0: - results.extra_info.return_code = process.returncode - results.termination_condition = TerminationCondition.error - results.solution_loader = SolSolutionLoader(None, None) + if len(nl_info.variables) == 0: + if len(nl_info.eliminated_vars) == 0: + results = IpoptResults() + results.termination_condition = TerminationCondition.emptyModel + results.solution_loader = SolSolutionLoader(None, None) + else: + results = IpoptResults() + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal + results.solution_loader = SolSolutionLoader(None, nl_info=nl_info) + results.iteration_count = 0 + results.timing_info.total_seconds = 0 else: - results.iteration_count = iters - results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc - results.timing_info.nlp_function_evaluations = ipopt_time_func - results.timing_info.total_seconds = ipopt_total_time + if os.path.isfile(basename + '.sol'): + with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') + results = self._parse_solution(sol_file, nl_info) + timer.stop('parse_sol') + else: + results = IpoptResults() + if process.returncode != 0: + results.extra_info.return_code = process.returncode + results.termination_condition = TerminationCondition.error + results.solution_loader = SolSolutionLoader(None, None) + else: + results.iteration_count = iters + results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc + results.timing_info.nlp_function_evaluations = ipopt_time_func + results.timing_info.total_seconds = ipopt_total_time if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -470,7 +494,8 @@ def solve(self, model, **kwds): ) results.solver_configuration = config - results.solver_log = ostreams[0].getvalue() + if len(nl_info.variables) > 0: + results.solver_log = ostreams[0].getvalue() # Capture/record end-time / wall-time end_timestamp = datetime.datetime.now(datetime.timezone.utc) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index f2c9cde64fe..88de0624629 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -73,6 +73,8 @@ class TerminationCondition(enum.Enum): license was found, the license is of the wrong type for the problem (e.g., problem is too big for type of license), or there was an issue contacting a licensing server. + emptyModel: 12 + The model being solved did not have any variables unknown: 42 All other unrecognized exit statuses fall in this category. """ @@ -101,6 +103,8 @@ class TerminationCondition(enum.Enum): licensingProblems = 11 + emptyModel = 12 + unknown = 42 diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 1812e21a596..5c971597789 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -14,6 +14,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr import value from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.sol_reader import SolFileData @@ -142,29 +143,48 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: - if self._nl_info.scaling: - for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling - ): - v.set_value(val / scale, skip_validation=True) + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if self._sol_data is None: + assert len(self._nl_info.variables) == 0 else: - for v, val in zip(self._nl_info.variables, self._sol_data.primals): - v.set_value(val, skip_validation=True) + if self._nl_info.scaling: + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling.variables + ): + v.set_value(val / scale, skip_validation=True) + else: + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) + + for v, v_expr in self._nl_info.eliminated_vars: + v.value = value(v_expr) StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: - if self._nl_info.scaling is None: - scale_list = [1] * len(self._nl_info.variables) - else: - scale_list = self._nl_info.scaling.variables + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) val_map = dict() - for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, scale_list - ): - val_map[id(v)] = val / scale + if self._sol_data is None: + assert len(self._nl_info.variables) == 0 + else: + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): + val_map[id(v)] = val / scale for v, v_expr in self._nl_info.eliminated_vars: val = replace_expressions(v_expr, substitution_map=val_map) @@ -184,18 +204,28 @@ def get_primals( def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if len(self._nl_info.eliminated_vars) > 0: + raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.') + assert self._sol_data is not None + res = dict() if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) + obj_scale = 1 else: scale_list = self._nl_info.scaling.constraints + obj_scale = self._nl_info.scaling.objectives[0] if cons_to_load is None: cons_to_load = set(self._nl_info.constraints) else: cons_to_load = set(cons_to_load) - res = dict() for c, val, scale in zip( self._nl_info.constraints, self._sol_data.duals, scale_list ): if c in cons_to_load: - res[c] = val * scale + res[c] = val * scale / obj_scale return res diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 2b9e783ad16..7f916c21dd7 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -36,26 +36,43 @@ nlp_solvers = [('ipopt', Ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] miqcqp_solvers = [('gurobi', Gurobi)] +nl_solvers = [('ipopt', Ipopt)] +nl_solvers_set = {i[0] for i in nl_solvers} def _load_tests(solver_list): res = list() for solver_name, solver in solver_list: - test_name = f"{solver_name}" - res.append((test_name, solver)) + if solver_name in nl_solvers_set: + test_name = f"{solver_name}_presolve" + res.append((test_name, solver, True)) + test_name = f"{solver_name}" + res.append((test_name, solver, False)) + else: + test_name = f"{solver_name}" + res.append((test_name, solver, None)) return res @unittest.skipUnless(numpy_available, 'numpy is not available') class TestSolvers(unittest.TestCase): + @parameterized.expand(input=all_solvers) + def test_config_overwrite(self, name: str, opt_class: Type[SolverBase]): + self.assertIsNot(SolverBase.CONFIG, opt_class.CONFIG) + @parameterized.expand(input=_load_tests(all_solvers)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[SolverBase] + self, name: str, opt_class: Type[SolverBase], use_presolve ): # this test is for issue #2888 opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) @@ -72,10 +89,15 @@ def test_remove_variable_and_objective( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): + def test_stale_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -113,10 +135,15 @@ def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): self.assertFalse(m.y.stale) @parameterized.expand(input=_load_tests(all_solvers)) - def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): + def test_range_constraint(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.obj = pe.Objective(expr=m.x) @@ -134,10 +161,15 @@ def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): + def test_reduced_costs(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var(bounds=(-2, 2)) @@ -156,10 +188,15 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(rc[m.y], -4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): + def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) @@ -176,10 +213,15 @@ def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_param_changes(self, name: str, opt_class: Type[SolverBase]): + def test_param_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -212,7 +254,7 @@ def test_param_changes(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): + def test_immutable_param(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. @@ -220,6 +262,11 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -252,12 +299,17 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_equality(self, name: str, opt_class: Type[SolverBase]): + def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') - if isinstance(opt, Ipopt): - opt.config.writer_config.linear_presolve = False + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + check_duals = False + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -285,15 +337,21 @@ def test_equality(self, name: str, opt_class: Type[SolverBase]): else: bound = res.objective_bound self.assertTrue(bound <= m.y.value) - duals = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): + def test_linear_expression(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -328,10 +386,17 @@ def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): self.assertTrue(bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers)) - def test_no_objective(self, name: str, opt_class: Type[SolverBase]): + def test_no_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + check_duals = False + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -354,15 +419,21 @@ def test_no_objective(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.incumbent_objective, None) self.assertEqual(res.objective_bound, None) - duals = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], 0) - self.assertAlmostEqual(duals[m.c2], 0) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): + def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -413,10 +484,15 @@ def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): + def test_results_infeasible(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -462,10 +538,15 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers)) - def test_duals(self, name: str, opt_class: Type[SolverBase]): + def test_duals(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -486,11 +567,16 @@ def test_duals(self, name: str, opt_class: Type[SolverBase]): @parameterized.expand(input=_load_tests(qcp_solvers)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[SolverBase] + self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -509,10 +595,15 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) @parameterized.expand(input=_load_tests(qcp_solvers)) - def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase]): + def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -534,7 +625,7 @@ def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): for treat_fixed_vars_as_params in [True, False]: opt: SolverBase = opt_class() if opt.is_persistent(): @@ -543,6 +634,11 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): ) if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -575,12 +671,17 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -613,12 +714,17 @@ def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -626,15 +732,21 @@ def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -649,10 +761,15 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 2**0.5) @parameterized.expand(input=_load_tests(all_solvers)) - def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): + def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False try: import numpy as np except: @@ -743,10 +860,15 @@ def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): + def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.y = pe.Var(bounds=(-1, None)) m.obj = pe.Objective(expr=m.y) @@ -789,10 +911,15 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, -1) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_exp(self, name: str, opt_class: Type[SolverBase]): + def test_exp(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -803,10 +930,15 @@ def test_exp(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_log(self, name: str, opt_class: Type[SolverBase]): + def test_log(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(initialize=1) m.y = pe.Var() @@ -817,10 +949,15 @@ def test_log(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, -0.42630274815985264) @parameterized.expand(input=_load_tests(all_solvers)) - def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): + def test_with_numpy(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -845,10 +982,15 @@ def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): + def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.y = pe.Var() m.p = pe.Param(mutable=True) @@ -877,10 +1019,15 @@ def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): + def test_solution_loader(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None)) m.y = pe.Var() @@ -927,10 +1074,15 @@ def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_time_limit(self, name: str, opt_class: Type[SolverBase]): + def test_time_limit(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False from sys import platform if platform == 'win32': @@ -983,10 +1135,15 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase]): ) @parameterized.expand(input=_load_tests(all_solvers)) - def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): + def test_objective_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -1047,10 +1204,15 @@ def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_domain(self, name: str, opt_class: Type[SolverBase]): + def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) @@ -1071,10 +1233,15 @@ def test_domain(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): + def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) @@ -1095,10 +1262,15 @@ def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(domain=pe.Binary) m.y = pe.Var() @@ -1122,10 +1294,15 @@ def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): + def test_with_gdp(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-10, 10)) @@ -1152,11 +1329,16 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) - @parameterized.expand(input=all_solvers) - def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): + @parameterized.expand(input=_load_tests(all_solvers)) + def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() @@ -1179,11 +1361,16 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) - @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): + @parameterized.expand(input=_load_tests(all_solvers)) + def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() @@ -1215,10 +1402,15 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_1(self, name: str, opt_class: Type[SolverBase]): + def test_bug_1(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(3, 7)) @@ -1238,7 +1430,7 @@ def test_bug_1(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_2(self, name: str, opt_class: Type[SolverBase]): + def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. @@ -1247,6 +1439,11 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option @@ -1266,6 +1463,63 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase]): res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, -18, 5) + @parameterized.expand(input=_load_tests(all_solvers)) + def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + check_duals = False + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= (m.x - 1) + 1) + m.c2 = pe.Constraint(expr=m.y >= -(m.x - 1) + 1) + m.scaling_factor = pe.Suffix(direction=pe.Suffix.EXPORT) + m.scaling_factor[m.x] = 0.5 + m.scaling_factor[m.y] = 2 + m.scaling_factor[m.c1] = 0.5 + m.scaling_factor[m.c2] = 2 + m.scaling_factor[m.obj] = 2 + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + primals = res.solution_loader.get_primals() + self.assertAlmostEqual(primals[m.x], 1) + self.assertAlmostEqual(primals[m.y], 1) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -0.5) + self.assertAlmostEqual(duals[m.c2], -0.5) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 0) + self.assertAlmostEqual(rc[m.y], 0) + + m.x.setlb(2) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 2) + primals = res.solution_loader.get_primals() + self.assertAlmostEqual(primals[m.x], 2) + self.assertAlmostEqual(primals[m.y], 2) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -1) + self.assertAlmostEqual(duals[m.c2], 0) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + self.assertAlmostEqual(rc[m.y], 0) + class TestLegacySolverInterface(unittest.TestCase): @parameterized.expand(input=all_solvers) From 2368ab94fc12d38eff15277ce4acd97c471cbd81 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 15:37:41 -0700 Subject: [PATCH 0414/1178] Work around strange deepcopy bug --- pyomo/common/collections/component_map.py | 2 +- pyomo/common/collections/component_set.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 0851ffad301..90d985990d1 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -16,7 +16,7 @@ def _rehash_keys(encode, val): if encode: - return list(val.values()) + return tuple(val.values()) else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index f1fe7bc8cd6..bad40e90195 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -18,7 +18,17 @@ def _rehash_keys(encode, val): if encode: - return list(val.values()) + # TBD [JDS 2/2024]: if we + # + # return list(val.values()) + # + # here, then we get a strange failure when deepcopying + # ComponentSets containing an _ImplicitAny domain. We could + # track it down to teh implementation of + # autoslots.fast_deepcopy, but couldn't find an obvious bug. + # There is no error if we just return the original dict, or if + # we return a tuple(val.values) + return tuple(val.values()) else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys From 0b6fd076de3a4b7bb48d924d3bce23f6913d689a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 15:41:04 -0700 Subject: [PATCH 0415/1178] NFC: fix spelling --- pyomo/common/autoslots.py | 2 +- pyomo/common/collections/component_set.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/common/autoslots.py b/pyomo/common/autoslots.py index cb79d4a0338..89fefaf4f21 100644 --- a/pyomo/common/autoslots.py +++ b/pyomo/common/autoslots.py @@ -29,7 +29,7 @@ def _deepcopy_tuple(obj, memo, _id): unchanged = False if unchanged: # Python does not duplicate "unchanged" tuples (i.e. allows the - # original objecct to be returned from deepcopy()). We will + # original object to be returned from deepcopy()). We will # preserve that behavior here. # # It also appears to be faster *not* to cache the fact that this diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index bad40e90195..d99dd694b64 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -24,7 +24,7 @@ def _rehash_keys(encode, val): # # here, then we get a strange failure when deepcopying # ComponentSets containing an _ImplicitAny domain. We could - # track it down to teh implementation of + # track it down to the implementation of # autoslots.fast_deepcopy, but couldn't find an obvious bug. # There is no error if we just return the original dict, or if # we return a tuple(val.values) From b9a6e8341da751c3b1ff6f834cfb110d8c5049d8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 15:46:45 -0700 Subject: [PATCH 0416/1178] error message --- pyomo/contrib/solver/solution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 5c971597789..e4734bd8a46 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -211,7 +211,7 @@ def get_duals( ) if len(self._nl_info.eliminated_vars) > 0: raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.') - assert self._sol_data is not None + assert self._sol_data is not None, "report this to the Pyomo developers" res = dict() if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) From 52391fc198bc4adc80b13ac8676f2cda9b1755d9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 15:52:23 -0700 Subject: [PATCH 0417/1178] Missed capitalization --- pyomo/contrib/solver/ipopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 074e5b19c5c..3ec69879675 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -47,7 +47,7 @@ logger = logging.getLogger(__name__) -class ipoptSolverError(PyomoException): +class IpoptSolverError(PyomoException): """ General exception to catch solver system errors """ @@ -315,7 +315,7 @@ def solve(self, model, **kwds): # Check if solver is available avail = self.available(config) if not avail: - raise ipoptSolverError( + raise IpoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) if config.threads: From bc1b3e9cec2266b7383627601511ac541212ffd4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 15:54:37 -0700 Subject: [PATCH 0418/1178] Apply black to updates --- pyomo/contrib/solver/ipopt.py | 12 +- pyomo/contrib/solver/solution.py | 8 +- .../solver/tests/solvers/test_solvers.py | 104 +++++++++++++----- 3 files changed, 93 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 876ca749921..537e6f85968 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -140,7 +140,9 @@ def get_reduced_costs( 'check the termination condition.' ) if len(self._nl_info.eliminated_vars) > 0: - raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.') + raise NotImplementedError( + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.' + ) assert self._sol_data is not None if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) @@ -417,7 +419,9 @@ def solve(self, model, **kwds): results.solution_loader = SolSolutionLoader(None, None) else: results = IpoptResults() - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) results.solution_status = SolutionStatus.optimal results.solution_loader = SolSolutionLoader(None, nl_info=nl_info) results.iteration_count = 0 @@ -436,7 +440,9 @@ def solve(self, model, **kwds): results.solution_loader = SolSolutionLoader(None, None) else: results.iteration_count = iters - results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc + results.timing_info.ipopt_excluding_nlp_functions = ( + ipopt_time_nofunc + ) results.timing_info.nlp_function_evaluations = ipopt_time_func results.timing_info.total_seconds = ipopt_total_time if ( diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index e4734bd8a46..7cef86a4e8f 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -153,7 +153,9 @@ def load_vars( else: if self._nl_info.scaling: for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling.variables + self._nl_info.variables, + self._sol_data.primals, + self._nl_info.scaling.variables, ): v.set_value(val / scale, skip_validation=True) else: @@ -210,7 +212,9 @@ def get_duals( 'check the termination condition.' ) if len(self._nl_info.eliminated_vars) > 0: - raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.') + raise NotImplementedError( + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.' + ) assert self._sol_data is not None, "report this to the Pyomo developers" res = dict() if self._nl_info.scaling is None: diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 7f916c21dd7..c6c73ea2dc7 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -89,7 +89,9 @@ def test_remove_variable_and_objective( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_stale_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_stale_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -135,7 +137,9 @@ def test_stale_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: self.assertFalse(m.y.stale) @parameterized.expand(input=_load_tests(all_solvers)) - def test_range_constraint(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_range_constraint( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -161,7 +165,9 @@ def test_range_constraint(self, name: str, opt_class: Type[SolverBase], use_pres self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_reduced_costs( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -188,7 +194,9 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase], use_presolv self.assertAlmostEqual(rc[m.y], -4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_reduced_costs2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -213,7 +221,9 @@ def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase], use_presol self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_param_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_param_changes( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -254,7 +264,9 @@ def test_param_changes(self, name: str, opt_class: Type[SolverBase], use_presolv self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_immutable_param(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_immutable_param( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. @@ -343,7 +355,9 @@ def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bo self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_linear_expression(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_linear_expression( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -386,7 +400,9 @@ def test_linear_expression(self, name: str, opt_class: Type[SolverBase], use_pre self.assertTrue(bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers)) - def test_no_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_no_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -425,7 +441,9 @@ def test_no_objective(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_add_remove_cons( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -484,7 +502,9 @@ def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase], use_preso self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_results_infeasible(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_results_infeasible( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -595,7 +615,9 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) @parameterized.expand(input=_load_tests(qcp_solvers)) - def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_mutable_quadratic_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -625,7 +647,9 @@ def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): for treat_fixed_vars_as_params in [True, False]: opt: SolverBase = opt_class() if opt.is_persistent(): @@ -671,7 +695,9 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars_2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -714,7 +740,9 @@ def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars_3( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -736,7 +764,9 @@ def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars_4( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -761,7 +791,9 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(m.y.value, 2**0.5) @parameterized.expand(input=_load_tests(all_solvers)) - def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_mutable_param_with_range( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -860,7 +892,9 @@ def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase], self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_add_and_remove_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -949,7 +983,9 @@ def test_log(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): self.assertAlmostEqual(m.y.value, -0.42630274815985264) @parameterized.expand(input=_load_tests(all_solvers)) - def test_with_numpy(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_with_numpy( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -982,7 +1018,9 @@ def test_with_numpy(self, name: str, opt_class: Type[SolverBase], use_presolve: self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_bounds_with_params( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1019,7 +1057,9 @@ def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase], use_pr self.assertAlmostEqual(m.y.value, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_solution_loader(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_solution_loader( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1074,7 +1114,9 @@ def test_solution_loader(self, name: str, opt_class: Type[SolverBase], use_preso self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_time_limit(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_time_limit( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1135,7 +1177,9 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase], use_presolve: ) @parameterized.expand(input=_load_tests(all_solvers)) - def test_objective_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_objective_changes( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1233,7 +1277,9 @@ def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_domain_with_integers( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1262,7 +1308,9 @@ def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase], use_ self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_binaries( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1330,7 +1378,9 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase], use_presolve: bo self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_variables_elsewhere( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1362,7 +1412,9 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase], use_p self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_variables_elsewhere2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') From ac7ce8b2bfbd06a1631f3f25a96ac0a48a4e7571 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 16:01:18 -0700 Subject: [PATCH 0419/1178] Update error messages --- pyomo/contrib/solver/solution.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 7cef86a4e8f..32e84d2abca 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -16,6 +16,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap +from pyomo.common.errors import DeveloperError from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.sol_reader import SolFileData from pyomo.repn.plugins.nl_writer import NLWriterInfo @@ -146,7 +147,7 @@ def load_vars( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) if self._sol_data is None: assert len(self._nl_info.variables) == 0 @@ -173,7 +174,7 @@ def get_primals( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) val_map = dict() if self._sol_data is None: @@ -209,13 +210,18 @@ def get_duals( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) if len(self._nl_info.eliminated_vars) > 0: raise NotImplementedError( - 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.' + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) ' + 'to get dual variable values.' + ) + if self._sol_data is None: + raise DeveloperError( + "Solution data is empty. This should not " + "have happened. Report this error to the Pyomo Developers." ) - assert self._sol_data is not None, "report this to the Pyomo developers" res = dict() if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) From 8f581df56f057101bf7fa41eb76c1ed87dd6b5e2 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 19 Feb 2024 16:17:42 -0700 Subject: [PATCH 0420/1178] Added __init__.py to parmest deprecated folder. --- pyomo/contrib/parmest/deprecated/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyomo/contrib/parmest/deprecated/__init__.py diff --git a/pyomo/contrib/parmest/deprecated/__init__.py b/pyomo/contrib/parmest/deprecated/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ From 48533dbc566525deb1c6f20acb93bdd394d8e162 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 16:18:10 -0700 Subject: [PATCH 0421/1178] NFC: update copyright --- pyomo/common/tests/test_component_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py index 9e771175d42..b9e2a953047 100644 --- a/pyomo/common/tests/test_component_map.py +++ b/pyomo/common/tests/test_component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 030aa576776959a0b52ad40ef3f04cad38b73b55 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 19 Feb 2024 16:32:11 -0700 Subject: [PATCH 0422/1178] More parmest black formatting. --- .../reactor_design/timeseries_data_example.py | 2 +- pyomo/contrib/parmest/parmest.py | 1 + pyomo/contrib/parmest/tests/test_parmest.py | 20 +++++-------------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 85095fb94de..1e457bf1e89 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -23,7 +23,7 @@ class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number - data_i = data.loc[data['experiment'] == experiment_number,:] + data_i = data.loc[data['experiment'] == experiment_number, :] self.data_i = data_i.reset_index() self.model = None diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ed9bda232b4..90d42e68910 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -271,6 +271,7 @@ def _experiment_instance_creation_callback( # return m + def SSE(model): expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 0893c9b4fde..15264a18989 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -76,9 +76,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append( - RooneyBieglerExperiment(data.loc[i, :]) - ) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) @@ -392,9 +390,7 @@ def create_model(self): rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_params_exp_list.append( - RooneyBieglerExperimentParams( - self.data.loc[i, :] - ) + RooneyBieglerExperimentParams(self.data.loc[i, :]) ) def rooney_biegler_indexed_params(data): @@ -440,9 +436,7 @@ def label_model(self): rooney_biegler_indexed_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( - RooneyBieglerExperimentIndexedParams( - self.data.loc[i, :] - ) + RooneyBieglerExperimentIndexedParams(self.data.loc[i, :]) ) def rooney_biegler_vars(data): @@ -521,9 +515,7 @@ def label_model(self): rooney_biegler_indexed_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( - RooneyBieglerExperimentIndexedVars( - self.data.loc[i, :] - ) + RooneyBieglerExperimentIndexedVars(self.data.loc[i, :]) ) # Sum of squared error function @@ -988,9 +980,7 @@ def SSE(model): exp_list = [] for i in range(data.shape[0]): - exp_list.append( - RooneyBieglerExperiment(data.loc[i, :]) - ) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) solver_options = {"tol": 1e-8} From 3828841bf5e327f69c5369acd9ce089e28a81751 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 16:33:21 -0700 Subject: [PATCH 0423/1178] Forgot targets for alt_wheels --- .github/workflows/release_wheel_creation.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 17152dc3d1e..932b0d8eea6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -68,6 +68,18 @@ jobs: os: [ubuntu-22.04] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] + + include: + - wheel-version: 'cp38*' + TARGET: 'py38' + - wheel-version: 'cp39*' + TARGET: 'py39' + - wheel-version: 'cp310*' + TARGET: 'py310' + - wheel-version: 'cp311*' + TARGET: 'py311' + - wheel-version: 'cp312*' + TARGET: 'py312' steps: - uses: actions/checkout@v4 - name: Set up QEMU From e9a99499d1b5b4184a2ada6accd160f67b4c7cd5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 16:40:52 -0700 Subject: [PATCH 0424/1178] Add DefaultComponentMap --- pyomo/common/collections/__init__.py | 2 +- pyomo/common/collections/component_map.py | 29 +++++++++++++++ pyomo/common/tests/test_component_map.py | 44 +++++++++++++++++++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/pyomo/common/collections/__init__.py b/pyomo/common/collections/__init__.py index 93785124e3c..717caf87b2c 100644 --- a/pyomo/common/collections/__init__.py +++ b/pyomo/common/collections/__init__.py @@ -14,6 +14,6 @@ from collections import UserDict from .orderedset import OrderedDict, OrderedSet -from .component_map import ComponentMap +from .component_map import ComponentMap, DefaultComponentMap from .component_set import ComponentSet from .bunch import Bunch diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 90d985990d1..be44bd6ff68 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -179,3 +179,32 @@ def setdefault(self, key, default=None): else: self[key] = default return default + + +class DefaultComponentMap(ComponentMap): + """A :py:class:`defaultdict` admitting Pyomo Components as keys + + This class is a replacement for defaultdict that allows Pyomo + modeling components to be used as entry keys. The base + implementation builds on :py:class:`ComponentMap`. + + """ + + __slots__ = ('default_factory',) + + def __init__(self, default_factory=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.default_factory = default_factory + + def __missing__(self, key): + if self.default_factory is None: + raise KeyError(key) + self[key] = ans = self.default_factory() + return ans + + def __getitem__(self, obj): + _key = _hasher[obj.__class__](obj) + if _key in self._dict: + return self._dict[_key][1] + else: + return self.__missing__(obj) diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py index b9e2a953047..7cd4ec2c458 100644 --- a/pyomo/common/tests/test_component_map.py +++ b/pyomo/common/tests/test_component_map.py @@ -11,8 +11,8 @@ import pyomo.common.unittest as unittest -from pyomo.common.collections import ComponentMap -from pyomo.environ import ConcreteModel, Var, Constraint +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap +from pyomo.environ import ConcreteModel, Block, Var, Constraint class TestComponentMap(unittest.TestCase): @@ -48,3 +48,43 @@ def test_tuple(self): self.assertNotIn((1, (2, i.v)), m.cm) self.assertIn((1, (2, m.v)), m.cm) self.assertNotIn((1, (2, m.v)), i.cm) + + +class TestDefaultComponentMap(unittest.TestCase): + def test_default_component_map(self): + dcm = DefaultComponentMap(ComponentSet) + + m = ConcreteModel() + m.x = Var() + m.b = Block() + m.b.y = Var() + + self.assertEqual(len(dcm), 0) + + dcm[m.x].add(m) + self.assertEqual(len(dcm), 1) + self.assertIn(m.x, dcm) + self.assertIn(m, dcm[m.x]) + + dcm[m.b.y].add(m.b) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertNotIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + dcm[m.b.y].add(m) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + def test_no_default_factory(self): + dcm = DefaultComponentMap() + + dcm['found'] = 5 + self.assertEqual(len(dcm), 1) + self.assertIn('found', dcm) + self.assertEqual(dcm['found'], 5) + + with self.assertRaisesRegex(KeyError, "'missing'"): + dcm["missing"] From cf6364cc870aa2050a49a9baba7687b1ceb45742 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 17:42:55 -0700 Subject: [PATCH 0425/1178] Additional attempt to resolve ComponentMap deepcopy --- pyomo/common/collections/component_map.py | 4 ++-- pyomo/common/collections/component_set.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index be44bd6ff68..c110e9f390b 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -16,11 +16,11 @@ def _rehash_keys(encode, val): if encode: - return tuple(val.values()) + return val else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val} + return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()} class _Hasher(collections.defaultdict): diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index d99dd694b64..19d2ef2f7f9 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -28,11 +28,11 @@ def _rehash_keys(encode, val): # autoslots.fast_deepcopy, but couldn't find an obvious bug. # There is no error if we just return the original dict, or if # we return a tuple(val.values) - return tuple(val.values()) + return val else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {_hasher[obj.__class__](obj): obj for obj in val} + return {_hasher[obj.__class__](obj): obj for obj in val.values()} class ComponentSet(AutoSlots.Mixin, collections_MutableSet): From 3f787a3f5a2fb772caf11468f267fa2968190ca7 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 06:52:47 -0700 Subject: [PATCH 0426/1178] Added exp1.out to exp14.out by force for deprecated semibatch examples in parmest. --- pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out | 1 + 14 files changed, 14 insertions(+) create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out new file mode 100644 index 00000000000..f1d826085bf --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out @@ -0,0 +1 @@ +{"experiment": 1, "Ca0": 0, "Ca_meas": {"17280.0": 1.0206137429621787, "0.0": 0.3179205033819153, "7560.0": 6.1190611079452015, "9720.0": 5.143125125521654, "19440.0": 0.6280402056097951, "16200.0": 0.8579867036528984, "11880.0": 3.7282923165210042, "2160.0": 4.63887641485678, "8640.0": 5.343033989137008, "14040.0": 2.053029378587664, "4320.0": 6.161603379127277, "6480.0": 6.175522427215327, "1080.0": 2.5735849991352358, "18360.0": 0.6351040530590654, "10800.0": 5.068206847862049, "21600.0": 0.40727295182614354, "20520.0": 0.6621175064161002, "5400.0": 6.567824349703669, "3240.0": 5.655458079751501, "12960.0": 2.654764659666162, "15120.0": 1.6757350275784135}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 5.987636915771096, "0.0": 0.09558280565339891, "7560.0": 1.238130321602845, "9720.0": 1.9945247030805529, "19440.0": 6.695864385679773, "16200.0": 5.41645220805244, "11880.0": 2.719892366798277, "2160.0": 0.10805070272409367, "8640.0": 1.800229763433655, "14040.0": 4.156268601598023, "4320.0": -0.044818714779864405, "6480.0": 0.8106022415380871, "1080.0": -0.07327388848369068, "18360.0": 5.96868114596425, "10800.0": 2.0726982059573835, "21600.0": 7.269213818513372, "20520.0": 6.725777234409265, "5400.0": 0.18749831830326769, "3240.0": -0.10164819461093579, "12960.0": 3.745361461163259, "15120.0": 4.92464438752146}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.527811727243744, "0.0": 0.006198551839384427, "7560.0": 8.198459268980448, "9720.0": 10.884163155983586, "19440.0": 12.150552321810109, "16200.0": 13.247640677577017, "11880.0": 12.921639059281906, "2160.0": 1.2393091651113075, "8640.0": 9.76716833273541, "14040.0": 13.211149989298647, "4320.0": 3.803104433804622, "6480.0": 6.5810650565269375, "1080.0": 0.3042714459761661, "18360.0": 12.544400522361945, "10800.0": 11.737104197836604, "21600.0": 11.886358606219954, "20520.0": 11.832544691029744, "5400.0": 5.213810980890077, "3240.0": 2.1926632109587216, "12960.0": 13.113839789286594, "15120.0": 13.27982750652159}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 325.16059207223026, "0.0": 297.66636064503314, "7560.0": 331.3122493868531, "9720.0": 332.5912691607457, "19440.0": 325.80525903012233, "16200.0": 323.2692288871708, "11880.0": 334.0549081870754, "2160.0": 323.2373236557714, "8640.0": 333.4576764024497, "14040.0": 328.42212335544315, "4320.0": 327.6704558317418, "6480.0": 331.06042780025075, "1080.0": 316.2567029216892, "18360.0": 326.6647586865489, "10800.0": 334.23136746878185, "21600.0": 324.0057232633869, "20520.0": 324.4555288383823, "5400.0": 331.9568676813546, "3240.0": 326.12583828081813, "12960.0": 329.2382904744002, "15120.0": 327.3959354386782}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out new file mode 100644 index 00000000000..7eb7980a7be --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out @@ -0,0 +1 @@ +{"experiment": 10, "Ca0": 0, "Ca_meas": {"0.0": 0.17585381115505311, "4320.0": 6.0315379232850992, "7560.0": 5.4904391073929908, "21600.0": 0.28272237229599306, "9720.0": 5.2677178238115365, "16200.0": 1.1265897978788217, "20520.0": 0.057584376823091032, "3240.0": 5.6315955838815661, "11880.0": 3.328489578835276, "14040.0": 2.0226562017072607, "17280.0": 1.1268539727440208, "2160.0": 4.3030132489621638, "5400.0": 6.1094034780709618, "18360.0": 0.50886621390394615, "8640.0": 5.3773941013828752, "6480.0": 5.9760510402178078, "1080.0": 2.9280667525762598, "15120.0": 1.375026987701359, "12960.0": 2.5451999496997635, "19440.0": 0.79349685535634917, "10800.0": 4.3653401523141229}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.038024312530073517, "4320.0": 0.35646997778490641, "7560.0": 1.0986283962244756, "21600.0": 7.160087143091391, "9720.0": 2.129148884681515, "16200.0": 5.1383561968992435, "20520.0": 6.8451793517536901, "3240.0": 0.098714783055484312, "11880.0": 3.0269500169512602, "14040.0": 3.9370558676788283, "17280.0": 5.9641262824404357, "2160.0": -0.12281730158248855, "5400.0": 0.59307341448224149, "18360.0": 6.2121451052794248, "8640.0": 1.7607685730069123, "6480.0": 0.53516134735284115, "1080.0": 0.021830284057365701, "15120.0": 4.7082270119144871, "12960.0": 4.0629501433813449, "19440.0": 6.333023537154518, "10800.0": 2.2805891192921983}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.063620932422370907, "4320.0": 3.5433262677921662, "7560.0": 8.6893371808300444, "21600.0": 11.870505989648386, "9720.0": 11.309193344250055, "16200.0": 12.739396897287321, "20520.0": 11.791007739959538, "3240.0": 2.1799284210951009, "11880.0": 12.669418985545658, "14040.0": 13.141014574935076, "17280.0": 12.645153211711902, "2160.0": 1.4830589148905116, "5400.0": 5.2739635750232985, "18360.0": 12.761210138866151, "8640.0": 9.9142303856203373, "6480.0": 7.0761548290524603, "1080.0": 0.43050918133895111, "15120.0": 12.66028651303237, "12960.0": 12.766057551719733, "19440.0": 12.385465894826957, "10800.0": 11.96758252080965}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.47604965958681, "4320.0": 327.40308974239883, "7560.0": 332.84954650085029, "21600.0": 322.5157157706418, "9720.0": 333.3451281132692, "16200.0": 325.64418049630734, "20520.0": 321.94339225425767, "3240.0": 327.19623764314332, "11880.0": 330.24784399520615, "14040.0": 328.20666366981681, "17280.0": 324.73232197103431, "2160.0": 324.1280979971192, "5400.0": 330.87716394833132, "18360.0": 323.47482388751507, "8640.0": 332.49299626233665, "6480.0": 332.1275559564059, "1080.0": 319.29329353123859, "15120.0": 327.29837672678497, "12960.0": 329.24537484541742, "19440.0": 324.02559861322572, "10800.0": 335.57159429203386}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out new file mode 100644 index 00000000000..d97442f031b --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out @@ -0,0 +1 @@ +{"experiment": 11, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9214639882977118, "7560.0": 3.782748575728669, "21600.0": 1.0934559688831289, "9720.0": 3.9988602724163731, "16200.0": 1.3975369118671503, "20520.0": 1.1243254110346068, "3240.0": 2.4246081576650433, "11880.0": 3.4755989173839743, "14040.0": 1.9594736461287787, "17280.0": 1.2827751626299488, "2160.0": 1.7884659182526941, "5400.0": 3.3009088212806073, "18360.0": 1.2111862780556402, "8640.0": 3.9172974313558302, "6480.0": 3.5822886585117493, "1080.0": 0.9878678077907459, "15120.0": 1.5972116122332332, "12960.0": 2.5920858659103394, "19440.0": 1.1618336860102094, "10800.0": 4.0378190270354528}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.023982318123109209, "7560.0": 0.12357850466238102, "21600.0": 6.5347744286121001, "9720.0": 0.24908724520575926, "16200.0": 3.2484340438243127, "20520.0": 5.9039946620569568, "3240.0": 0.0099959742916886744, "11880.0": 0.58037814751790051, "14040.0": 1.8149241834100336, "17280.0": 3.9360275733370447, "2160.0": 0.0028162554877217529, "5400.0": 0.046614709277751604, "18360.0": 4.6059327483829717, "8640.0": 0.17995429953061945, "6480.0": 0.079417046516631534, "1080.0": 0.00029995464126276485, "15120.0": 2.5396785773426069, "12960.0": 1.1193190878564474, "19440.0": 5.2613272888405183, "10800.0": 0.33137635542084787}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.96588936445839646, "7560.0": 2.5063363714379272, "21600.0": 7.0191274767686718, "9720.0": 3.6738114082888234, "16200.0": 7.2205659498627206, "20520.0": 7.0929480434376666, "3240.0": 0.56824903663740756, "11880.0": 5.2689545424581627, "14040.0": 6.8616827734843717, "17280.0": 7.2356915893004699, "2160.0": 0.25984959276122965, "5400.0": 1.4328144788513235, "18360.0": 7.2085405027857679, "8640.0": 3.0841896398822519, "6480.0": 1.9514434452676097, "1080.0": 0.063681239951285565, "15120.0": 7.1238797146821753, "12960.0": 6.279843675978368, "19440.0": 7.1578041487575703, "10800.0": 4.2664529460540459}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.23882460110946, "7560.0": 313.65588947948061, "21600.0": 347.47264008527429, "9720.0": 314.23459379740643, "16200.0": 349.53974420970837, "20520.0": 347.6739307915775, "3240.0": 311.5507050941913, "11880.0": 343.84565806826117, "14040.0": 351.77400891772504, "17280.0": 348.76203275606656, "2160.0": 310.56857709679934, "5400.0": 312.79850843986321, "18360.0": 348.2596349485566, "8640.0": 313.9757607933924, "6480.0": 313.26649244154709, "1080.0": 308.41052123547064, "15120.0": 350.65940715239384, "12960.0": 351.11078111562267, "19440.0": 347.92214750818005, "10800.0": 317.60632266285268}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out new file mode 100644 index 00000000000..cbed2d89634 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out @@ -0,0 +1 @@ +{"experiment": 12, "Ca0": 0, "Ca_meas": {"0.0": -0.11670041274142862, "4320.0": 2.5843604031503551, "7560.0": 3.9909358380362421, "21600.0": 0.85803342268297711, "9720.0": 3.8011636431252702, "16200.0": 1.1002575863333801, "20520.0": 1.5248654901477563, "3240.0": 2.8626873137030655, "11880.0": 3.0876088469734766, "14040.0": 2.1233520630854348, "17280.0": 1.3118009790549121, "2160.0": 1.9396713478350336, "5400.0": 3.4898817799323192, "18360.0": 1.3214498335778544, "8640.0": 4.3166520687122008, "6480.0": 3.7430014080130212, "1080.0": 0.71502131244112932, "15120.0": 1.6869248126824714, "12960.0": 2.6199132141077999, "19440.0": 1.1934026369102775, "10800.0": 4.1662760005238244}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.14625373198142194, "4320.0": 0.50521240190855021, "7560.0": 0.072658369151157948, "21600.0": 6.5820277701997947, "9720.0": 0.14656574869775074, "16200.0": 3.2344935292984842, "20520.0": 6.1934992398225113, "3240.0": 0.073187422926812754, "11880.0": 0.68271223751792398, "14040.0": 1.5495094265280573, "17280.0": 3.5758298262936474, "2160.0": -0.052665808995189155, "5400.0": -0.067101579134353218, "18360.0": 4.6809081069906799, "8640.0": 0.58099071333349073, "6480.0": -0.34905530826754594, "1080.0": -0.13981627097780677, "15120.0": 2.7577781806493582, "12960.0": 0.9824219783559841, "19440.0": 5.2002977724609245, "10800.0": 0.0089889440138224419}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.34864609349591541, "4320.0": 0.99561164186682227, "7560.0": 2.4688230226633143, "21600.0": 7.0564292657160461, "9720.0": 3.9961025255558669, "16200.0": 7.161739218807984, "20520.0": 6.8044634236188921, "3240.0": 0.21017912447011355, "11880.0": 5.1168427571991435, "14040.0": 7.0032016822280907, "17280.0": 7.11845364876228, "2160.0": 0.22241873726625405, "5400.0": 1.3174801426799267, "18360.0": 6.9581816529657257, "8640.0": 3.0438444011178785, "6480.0": 2.2558063612162291, "1080.0": 0.38969247867806489, "15120.0": 7.2125210995495488, "12960.0": 6.4014182164755429, "19440.0": 6.8128450220608308, "10800.0": 4.2649851849420299}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.53274252012955, "4320.0": 311.15339067777529, "7560.0": 312.18867239090878, "21600.0": 346.00646319366473, "9720.0": 313.61710665630221, "16200.0": 352.81825567952984, "20520.0": 346.66248325950249, "3240.0": 311.37928873928001, "11880.0": 343.17457873193757, "14040.0": 352.88609842940627, "17280.0": 348.48519596719899, "2160.0": 312.70676562686674, "5400.0": 314.29841143358993, "18360.0": 347.81967845794014, "8640.0": 313.26528616120316, "6480.0": 314.33539425367189, "1080.0": 308.35955588183077, "15120.0": 350.62179387183005, "12960.0": 348.72513776404708, "19440.0": 346.65341312375318, "10800.0": 318.79995964600641}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out new file mode 100644 index 00000000000..6ef514c951a --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out @@ -0,0 +1 @@ +{"experiment": 13, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9015294551493156, "7560.0": 3.7456240714596114, "21600.0": 1.0893473942847287, "9720.0": 3.955661759695563, "16200.0": 1.3896514099314119, "20520.0": 1.1200835239496501, "3240.0": 2.4116751003091821, "11880.0": 3.4212519616089123, "14040.0": 1.9331934810250495, "17280.0": 1.2772682671606199, "2160.0": 1.7821342925311514, "5400.0": 3.2743367747379883, "18360.0": 1.2065189956942144, "8640.0": 3.8765800278544793, "6480.0": 3.5499054339456388, "1080.0": 0.98643229677676525, "15120.0": 1.583391359829623, "12960.0": 2.5471212725882397, "19440.0": 1.1574522594063252, "10800.0": 3.9931029485836738}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.025294213033754839, "7560.0": 0.12897041691694733, "21600.0": 6.5640983670402253, "9720.0": 0.2583989163781088, "16200.0": 3.2753828012649455, "20520.0": 5.9325635171128175, "3240.0": 0.01057908838025531, "11880.0": 0.60077017176350533, "14040.0": 1.8465767045073862, "17280.0": 3.9624456898675042, "2160.0": 0.0029862658207987017, "5400.0": 0.048985517519306479, "18360.0": 4.6327886545170172, "8640.0": 0.1872203610374778, "6480.0": 0.083160507067847278, "1080.0": 0.0003160950335645278, "15120.0": 2.5686484578016535, "12960.0": 1.149697840345306, "19440.0": 5.2890172904133799, "10800.0": 0.3428648031290083}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.98451200269614558, "7560.0": 2.5380689634524152, "21600.0": 6.993912112938939, "9720.0": 3.7076982498372795, "16200.0": 7.2015026943578233, "20520.0": 7.0686210754667576, "3240.0": 0.58059897990470211, "11880.0": 5.3029094739876195, "14040.0": 6.8563104174907483, "17280.0": 7.2147803682393352, "2160.0": 0.26601120814969509, "5400.0": 1.4570157171523839, "18360.0": 7.1863518790131433, "8640.0": 3.1176409818767401, "6480.0": 1.9800832092825005, "1080.0": 0.06510061057296454, "15120.0": 7.1087300866267373, "12960.0": 6.294429516811606, "19440.0": 7.1344955737885876, "10800.0": 4.2996805767976616}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.83405362164399, "7560.0": 314.10857941130064, "21600.0": 347.58975942376372, "9720.0": 314.61233912755387, "16200.0": 349.56203914424219, "20520.0": 347.79317623855678, "3240.0": 312.2016171592112, "11880.0": 344.43453363173575, "14040.0": 351.72789167129031, "17280.0": 348.83759825667926, "2160.0": 311.27622091507072, "5400.0": 313.34232236658977, "18360.0": 348.36453268948424, "8640.0": 314.38892058203726, "6480.0": 313.76278431242508, "1080.0": 309.11491437259622, "15120.0": 350.61679816631096, "12960.0": 351.27269989378158, "19440.0": 348.03901542922108, "10800.0": 318.0555419842903}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out new file mode 100644 index 00000000000..cc3f95da860 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out @@ -0,0 +1 @@ +{"experiment": 14, "Ca0": 0, "Ca_meas": {"0.0": 0.081520066870384211, "4320.0": 2.8337428652268013, "7560.0": 3.9530051428610888, "21600.0": 1.1807185641508762, "9720.0": 4.0236480913821637, "16200.0": 1.4376034521825525, "20520.0": 1.5413859123725682, "3240.0": 2.7181227248994633, "11880.0": 3.3903617547506242, "14040.0": 2.0159168723544196, "17280.0": 1.0638207528750085, "2160.0": 1.8959521419461316, "5400.0": 2.9705863021885124, "18360.0": 1.0674705545585839, "8640.0": 4.144391992823846, "6480.0": 3.5918471023904477, "1080.0": 1.0113708479421195, "15120.0": 1.6541373379275581, "12960.0": 2.6476139688086877, "19440.0": 1.2312633238777129, "10800.0": 3.8743606114727154}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.089503442052066576, "4320.0": 0.18208896954493076, "7560.0": 0.17697219964055824, "21600.0": 6.8780014679710426, "9720.0": 0.35693081777790492, "16200.0": 3.1977597178279002, "20520.0": 6.1361982627818481, "3240.0": 0.10972700122863582, "11880.0": 0.76070080263615369, "14040.0": 1.5671921543009262, "17280.0": 4.0972632572434122, "2160.0": -0.28322371444225319, "5400.0": -0.079382269802482266, "18360.0": 4.5706426776745905, "8640.0": 0.26888423813019369, "6480.0": 0.1979519809989283, "1080.0": 0.13979150229071807, "15120.0": 2.6867100129129424, "12960.0": 0.99472739483139283, "19440.0": 5.6554142424694902, "10800.0": 0.46709816548507599}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.1139198104669558, "4320.0": 0.99036932502879282, "7560.0": 2.4258194495364482, "21600.0": 7.0446273994379434, "9720.0": 3.7506516413639113, "16200.0": 6.9192013751011503, "20520.0": 7.1555299371654808, "3240.0": 0.66230857796133746, "11880.0": 5.0716652323781499, "14040.0": 7.0971130388695007, "17280.0": 7.4091470358534082, "2160.0": -0.039078609338807413, "5400.0": 1.480378464133409, "18360.0": 7.1741052031883399, "8640.0": 3.4996110541636019, "6480.0": 2.0450173775271647, "1080.0": 0.13728827557251419, "15120.0": 6.7382150794212539, "12960.0": 6.3936782268937753, "19440.0": 7.3298178049407321, "10800.0": 4.6925893428035463}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.43741888919669, "4320.0": 313.81215912108536, "7560.0": 313.22398065446896, "21600.0": 347.95979639817102, "9720.0": 316.38480427739029, "16200.0": 350.46267406552954, "20520.0": 349.96840569755773, "3240.0": 313.32903259260996, "11880.0": 345.61089172333249, "14040.0": 352.83446396320579, "17280.0": 347.23320123776085, "2160.0": 310.02897702317114, "5400.0": 314.70707924235739, "18360.0": 349.8524737596988, "8640.0": 314.13917383134913, "6480.0": 314.15959183349207, "1080.0": 307.97982604287193, "15120.0": 349.00176197155969, "12960.0": 350.41651244789142, "19440.0": 346.1591726550746, "10800.0": 317.90588794308121}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out new file mode 100644 index 00000000000..e5245dff3f0 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out @@ -0,0 +1 @@ +{"experiment": 2, "Ca0": 0, "Ca_meas": {"12960.0": 2.6833775561963225, "21600.0": 1.1452663167259416, "17280.0": 1.2021595324165524, "19440.0": 1.0621392247792012, "0.0": -0.1316904398446574, "7560.0": 3.544605362880795, "11880.0": 3.1817551426501267, "14040.0": 2.066815570405579, "4320.0": 3.0488618589043432, "15120.0": 1.5896211475539537, "1080.0": 0.8608182979507091, "18360.0": 1.1317484585922248, "8640.0": 3.454602822099547, "2160.0": 1.7479951078246254, "20520.0": 1.2966191801491087, "9720.0": 4.0596636929917285, "6480.0": 3.9085446597134283, "3240.0": 2.5050366860794875, "5400.0": 3.2668528110981576, "10800.0": 4.004828727345138, "16200.0": 1.2860720212507326}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"12960.0": 1.3318254182147888, "21600.0": 6.431089735352747, "17280.0": 3.9711701719678825, "19440.0": 5.321278981143728, "0.0": 0.10325037628575211, "7560.0": 0.10827803632010198, "11880.0": 0.5184920846420157, "14040.0": 1.7974496302186054, "4320.0": 0.03112694971654564, "15120.0": 2.5245584142423207, "1080.0": 0.27315169217241275, "18360.0": 4.587420104936772, "8640.0": 0.1841080751926184, "2160.0": -0.3018405210283593, "20520.0": 6.0086124912796794, "9720.0": 0.08100716578409362, "6480.0": 0.027897809479352567, "3240.0": 0.01973928836607919, "5400.0": 0.02694434057766582, "10800.0": 0.3021977838614568, "16200.0": 3.561159267372319}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"12960.0": 6.358162874770003, "21600.0": 7.190121324797683, "17280.0": 7.302887809357959, "19440.0": 7.249179120248633, "0.0": -0.2300456054526237, "7560.0": 2.5654112157969364, "11880.0": 5.3061363321784025, "14040.0": 6.84009925060659, "4320.0": 0.9644475554073758, "15120.0": 7.261439274435592, "1080.0": 0.2644823572584467, "18360.0": 7.3015136544611305, "8640.0": 3.065189548359326, "2160.0": 0.27122113581760515, "20520.0": 7.162520282520871, "9720.0": 3.701445814227159, "6480.0": 2.0255650533089735, "3240.0": 0.48891328422133334, "5400.0": 1.326135697891304, "10800.0": 4.031252283597449, "16200.0": 7.38249778574694}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"12960.0": 350.10381529626676, "21600.0": 345.04247547236935, "17280.0": 348.24859807524, "19440.0": 346.7076647696431, "0.0": 301.30135373385, "7560.0": 314.7140350191631, "11880.0": 343.608488145403, "14040.0": 352.2902404657956, "4320.0": 312.2349716351422, "15120.0": 351.28330527232634, "1080.0": 307.67178366332996, "18360.0": 347.3517733033304, "8640.0": 313.62143853358026, "2160.0": 310.2674222024707, "20520.0": 348.1662021459614, "9720.0": 314.8081875940964, "6480.0": 312.7396303616638, "3240.0": 310.9258419605079, "5400.0": 312.8448509580561, "10800.0": 317.33866255037736, "16200.0": 349.5494112095972}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out new file mode 100644 index 00000000000..a9b013d476f --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out @@ -0,0 +1 @@ +{"experiment": 3, "Ca0": 0, "Ca_meas": {"17280.0": 0.39426558381033217, "0.0": -0.25761950998771116, "7560.0": 7.638659217862309, "9720.0": 7.741721088143662, "19440.0": 0.12706787288438182, "16200.0": 0.15060928317089423, "11880.0": 4.534965302380243, "2160.0": 4.47101661859487, "8640.0": 7.562734803826617, "14040.0": 0.8456407304976143, "4320.0": 7.395023123698018, "6480.0": 7.952409415603349, "1080.0": 3.0448009666821947, "18360.0": 0.0754742404427045, "10800.0": 7.223420802364689, "21600.0": 0.181946676125186, "20520.0": 0.195256504023462, "5400.0": 7.844394030136843, "3240.0": 6.031994466757849, "12960.0": 1.813573590958129, "15120.0": 0.30408071219857113}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"17280.0": 10.587758291638856, "0.0": -0.21796531802425348, "7560.0": 0.8680716299725404, "9720.0": 0.9085250272598128, "19440.0": 11.914154788848554, "16200.0": 9.737618534040658, "11880.0": 2.0705302921286064, "2160.0": -0.022903632391514165, "8640.0": 0.5195918959805059, "14040.0": 6.395605356788582, "4320.0": 0.010107836996695638, "6480.0": 0.09956884355869228, "1080.0": -0.21178603534213003, "18360.0": 10.990013628196317, "10800.0": 1.345982231414325, "21600.0": 12.771296192955955, "20520.0": 12.196513048345082, "5400.0": 0.04790148745481311, "3240.0": 0.318546588876358, "12960.0": 4.693433882970767, "15120.0": 8.491695125145553}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 8.731932027503914, "0.0": -0.1375816812387338, "7560.0": 6.8484762842626985, "9720.0": 9.333087689786101, "19440.0": 7.381577268709603, "16200.0": 9.502048821225666, "11880.0": 12.406853134672218, "2160.0": 0.8107944776900446, "8640.0": 8.158484571318013, "14040.0": 11.54445651179274, "4320.0": 2.8119825114954082, "6480.0": 5.520857819630275, "1080.0": 0.18414413253133835, "18360.0": 8.145712620219781, "10800.0": 10.75765409121092, "21600.0": 6.1356948865706356, "20520.0": 6.576247039355788, "5400.0": 3.92591568907661, "3240.0": 2.015632242014947, "12960.0": 12.71320139030468, "15120.0": 10.314039809497785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 345.7556825478521, "0.0": 300.1125978749429, "7560.0": 319.7301093967675, "9720.0": 321.4474947517745, "19440.0": 345.1982881134514, "16200.0": 348.50691612993586, "11880.0": 357.2800598685144, "2160.0": 311.7652063627056, "8640.0": 323.6663990115871, "14040.0": 365.2497105829804, "4320.0": 317.1702037696461, "6480.0": 319.9056594806601, "1080.0": 309.6187369359568, "18360.0": 345.1427626681205, "10800.0": 323.9154289387584, "21600.0": 345.45022165546357, "20520.0": 344.5991879566869, "5400.0": 316.21958050333416, "3240.0": 314.95895736530616, "12960.0": 371.5669986462856, "15120.0": 355.3612054360245}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out new file mode 100644 index 00000000000..e702db7d05b --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out @@ -0,0 +1 @@ +{"experiment": 4, "Ca0": 0, "Ca_meas": {"17280.0": 1.1816674202534045, "0.0": -0.25414468591124584, "7560.0": 5.914582094251343, "9720.0": 5.185067133371561, "19440.0": 0.5307435290768995, "16200.0": 1.2011215994628408, "11880.0": 3.5778183914967925, "2160.0": 4.254440534150475, "8640.0": 5.473440610227645, "14040.0": 2.1475894664278354, "4320.0": 5.8795707148110266, "6480.0": 6.089523479429854, "1080.0": 2.4339586418303543, "18360.0": 0.545228377126232, "10800.0": 4.946406396626746, "21600.0": 0.3169450590438124, "20520.0": 0.5859997070045333, "5400.0": 6.15901928205937, "3240.0": 5.5559094344993225, "12960.0": 2.476561130612629, "15120.0": 1.35232620260846}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.529641678005382, "0.0": 0.23057916607631007, "7560.0": 1.4209973582182187, "9720.0": 2.662002861314475, "19440.0": 7.612677904908142, "16200.0": 6.236740105453746, "11880.0": 3.6132373498432813, "2160.0": 0.41778377045750303, "8640.0": 2.3031169482702336, "14040.0": 4.857027337132376, "4320.0": 0.21846387467958883, "6480.0": 1.304912741118713, "1080.0": -0.002497120213976349, "18360.0": 7.207560113722179, "10800.0": 3.072350404943197, "21600.0": 8.437070128901182, "20520.0": 7.985790844633096, "5400.0": 0.5552902218354748, "3240.0": 0.4207298617922146, "12960.0": 4.791797355546968, "15120.0": 5.544868662346418}, "alphaj": 0.8, "Cb0": 2, "Vr0": 1, "Cb_meas": {"17280.0": 13.559468310792148, "0.0": 2.1040937779048963, "7560.0": 9.809117250733637, "9720.0": 12.404875593478181, "19440.0": 12.616477055699818, "16200.0": 13.94157106495499, "11880.0": 13.828382570964388, "2160.0": 3.1373485614417618, "8640.0": 11.053587506450443, "14040.0": 14.267859106799012, "4320.0": 5.6037726942190424, "6480.0": 8.499893646580981, "1080.0": 2.2930856900001535, "18360.0": 13.566545045105496, "10800.0": 13.397445458860116, "21600.0": 12.347983697813623, "20520.0": 12.422291796055749, "5400.0": 7.367727360896684, "3240.0": 3.8542518870239824, "12960.0": 14.038139660530316, "15120.0": 14.114308276615096}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.4465785902298, "0.0": 299.6733512720839, "7560.0": 333.04897592093835, "9720.0": 334.48916083151335, "19440.0": 323.5321646227951, "16200.0": 326.84528144052564, "11880.0": 332.77528830002086, "2160.0": 322.2453586799791, "8640.0": 334.2512423463752, "14040.0": 329.05431569837447, "4320.0": 327.99896899102527, "6480.0": 334.3262547857335, "1080.0": 317.32350372398014, "18360.0": 324.97573570445866, "10800.0": 334.23107235994195, "21600.0": 323.6424061352077, "20520.0": 324.00045995355015, "5400.0": 331.45112696032044, "3240.0": 326.33322784448785, "12960.0": 330.4778004752374, "15120.0": 326.8776604963411}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out new file mode 100644 index 00000000000..6c4b1b1d9e0 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out @@ -0,0 +1 @@ +{"experiment": 5, "Ca0": 0, "Ca_meas": {"17280.0": 0.5384747196579402, "0.0": -0.14396911833443257, "7560.0": 6.038385982663388, "9720.0": 5.0792329539506, "19440.0": 0.3782801126758533, "16200.0": 1.0619887309834395, "11880.0": 3.6494330494296436, "2160.0": 4.775401751873804, "8640.0": 5.629577532845656, "14040.0": 2.0037718871692265, "4320.0": 5.889802624055117, "6480.0": 6.09724817816528, "1080.0": 2.875851853145854, "18360.0": 0.6780066197547887, "10800.0": 4.859469684381779, "21600.0": 0.3889400954173796, "20520.0": 0.3351378562274788, "5400.0": 6.127222815180268, "3240.0": 5.289726682847115, "12960.0": 2.830845316709853, "15120.0": 1.5312992911111707}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 2, "Tc1": 320, "Cc_meas": {"17280.0": 7.596710556194337, "0.0": 1.6504080367065743, "7560.0": 2.809410585275859, "9720.0": 3.8419183958835132, "19440.0": 8.137782633931637, "16200.0": 6.938967086259325, "11880.0": 5.022162589071362, "2160.0": 2.0515545922033964, "8640.0": 3.506455726732785, "14040.0": 6.010539749263416, "4320.0": 2.2056993474658584, "6480.0": 2.5775763528099858, "1080.0": 2.03522693402577, "18360.0": 8.083917616781594, "10800.0": 4.662851778068136, "21600.0": 9.279674687903626, "20520.0": 8.963676424956157, "5400.0": 2.293408505844697, "3240.0": 1.9216270432789067, "12960.0": 5.637375563057352, "15120.0": 6.3296720972633045}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.831256801432378, "0.0": 0.13194621122245662, "7560.0": 7.965534934436229, "9720.0": 10.908595985103954, "19440.0": 12.408596390398941, "16200.0": 12.975405069340143, "11880.0": 12.710800046234393, "2160.0": 0.9223242691530996, "8640.0": 9.454601468197033, "14040.0": 13.19437793062601, "4320.0": 3.713168763161746, "6480.0": 6.515936097446724, "1080.0": 0.031354105110323494, "18360.0": 12.821094923672087, "10800.0": 11.90520078370877, "21600.0": 11.81429953673305, "20520.0": 12.099271866573613, "5400.0": 4.982941965055916, "3240.0": 2.766378581935415, "12960.0": 13.074621618364043, "15120.0": 12.957819319226212}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 326.8759279818479, "0.0": 300.7736745288814, "7560.0": 333.6674031533901, "9720.0": 333.59854139744135, "19440.0": 324.5264772316974, "16200.0": 325.2454701315101, "11880.0": 332.9849253092768, "2160.0": 322.1940607068012, "8640.0": 331.78378240085084, "14040.0": 328.48981010099453, "4320.0": 327.3883510651506, "6480.0": 330.15101610436426, "1080.0": 318.24994073025096, "18360.0": 323.9527212120804, "10800.0": 333.3006916263996, "21600.0": 322.07065855783964, "20520.0": 324.3518907083261, "5400.0": 331.5429008148077, "3240.0": 324.52116111644654, "12960.0": 329.21899337854876, "15120.0": 328.26179934031467}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out new file mode 100644 index 00000000000..c1630902e1a --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out @@ -0,0 +1 @@ +{"experiment": 6, "Ca0": 2, "Ca_meas": {"17280.0": 1.1595382970987758, "0.0": 1.9187666718004224, "7560.0": 5.977461039170304, "9720.0": 5.164472215594892, "19440.0": 0.6528636977624275, "16200.0": 1.2106046606700225, "11880.0": 3.3229659243191296, "2160.0": 5.923887627906124, "8640.0": 5.225477003110976, "14040.0": 1.7878931129582107, "4320.0": 6.782806717544953, "6480.0": 6.27323507174512, "1080.0": 4.481633914987097, "18360.0": 0.8866911582721309, "10800.0": 4.481150474336123, "21600.0": 0.2170007972283953, "20520.0": 0.3199825651255196, "5400.0": 6.795886093698936, "3240.0": 6.288606047308427, "12960.0": 2.4509424000990685, "15120.0": 1.506568611506372}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.588306395438309, "0.0": 0.24402401145820712, "7560.0": 1.4036889138374646, "9720.0": 2.4218855673847455, "19440.0": 7.764492558630308, "16200.0": 6.205315919403138, "11880.0": 3.593219427441702, "2160.0": -0.10553376629311664, "8640.0": 1.8628128392103824, "14040.0": 5.027532358914124, "4320.0": 0.2172961549286831, "6480.0": 0.875637414228913, "1080.0": 0.2688503672636328, "18360.0": 7.240866507350995, "10800.0": 3.26514503365032, "21600.0": 8.251445433411781, "20520.0": 7.987953548408583, "5400.0": 0.6458428001299884, "3240.0": 0.3201498579399834, "12960.0": 4.263491165240245, "15120.0": 5.885223860529403}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 13.604962664333257, "0.0": -0.20747860874385013, "7560.0": 10.013416230907982, "9720.0": 12.055665770517061, "19440.0": 13.289064414490856, "16200.0": 13.82240671329648, "11880.0": 13.783622629658064, "2160.0": 1.8287780310400052, "8640.0": 11.17018006067254, "14040.0": 14.064985893141849, "4320.0": 5.072613174963567, "6480.0": 8.597613514631933, "1080.0": 0.3932885074677299, "18360.0": 13.473844971871975, "10800.0": 13.343451941795923, "21600.0": 12.209822751574375, "20520.0": 12.483108442400093, "5400.0": 6.7290370118557545, "3240.0": 3.4163314305947527, "12960.0": 14.296996898861073, "15120.0": 14.543019390786785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.3952638382081, "0.0": 300.9417777707309, "7560.0": 334.57634369064954, "9720.0": 336.13718804612154, "19440.0": 324.10564173791454, "16200.0": 324.97714743647435, "11880.0": 332.29384802281055, "2160.0": 324.243456129639, "8640.0": 335.85007440436317, "14040.0": 329.01187645109906, "4320.0": 331.1961476255781, "6480.0": 333.16262386818596, "1080.0": 319.0632107387995, "18360.0": 322.11836267923206, "10800.0": 332.8894634515628, "21600.0": 323.92451205164855, "20520.0": 323.319714630304, "5400.0": 334.21206737651613, "3240.0": 326.78695915581983, "12960.0": 329.6184998003745, "15120.0": 327.5414299857002}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out new file mode 100644 index 00000000000..6ef879f3a17 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out @@ -0,0 +1 @@ +{"experiment": 7, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out new file mode 100644 index 00000000000..6aa9fea17b3 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out @@ -0,0 +1 @@ +{"experiment": 8, "Ca0": 0, "Ca_meas": {"0.0": 0.30862088766711671, "4320.0": 6.0491110491659228, "7560.0": 5.7909310485601502, "21600.0": 0.232444399226299, "9720.0": 4.9320449060797475, "16200.0": 0.97134242753331668, "20520.0": 0.42847724332841963, "3240.0": 5.6988320807198498, "11880.0": 3.3235733576868514, "14040.0": 1.9846460628194049, "17280.0": 0.87715206210722585, "2160.0": 4.615346351863904, "5400.0": 6.384056703029386, "18360.0": 0.41688144324552118, "8640.0": 5.4121173109702099, "6480.0": 6.0660731346226324, "1080.0": 2.8379509025410488, "15120.0": 0.98831570466285279, "12960.0": 2.2167483934357417, "19440.0": 0.46284950985984985, "10800.0": 4.7377220491627412}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.050833820035522316, "4320.0": 0.21055066883154505, "7560.0": 1.3851246436045646, "21600.0": 7.3672985895890362, "9720.0": 2.0100502709379842, "16200.0": 5.1793087406376159, "20520.0": 6.840381847429823, "3240.0": 0.13411276227648503, "11880.0": 3.3052152545385454, "14040.0": 3.9431305823279708, "17280.0": 5.7290141848801586, "2160.0": 0.16719633749951002, "5400.0": 0.49872603502453117, "18360.0": 6.1508540969551078, "8640.0": 1.4737312345987361, "6480.0": 0.69437977126769512, "1080.0": -0.0093978134715377304, "15120.0": 4.9151661032041298, "12960.0": 4.0623539766149843, "19440.0": 6.3058400571478561, "10800.0": 2.820347355873587}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.037166728983599892, "4320.0": 3.5183932586092856, "7560.0": 8.916682434428397, "21600.0": 11.534104657089006, "9720.0": 11.087958791247285, "16200.0": 13.02597376112109, "20520.0": 11.639999923137731, "3240.0": 2.346958004261503, "11880.0": 12.049613604010537, "14040.0": 12.906738918465997, "17280.0": 12.867691165140879, "2160.0": 1.3313958783841602, "5400.0": 5.3650409213472221, "18360.0": 12.405763004965722, "8640.0": 9.5635832344445717, "6480.0": 7.0954049721214671, "1080.0": 0.40883709280782765, "15120.0": 12.971506554625082, "12960.0": 12.829158718434032, "19440.0": 11.946615137583075, "10800.0": 11.373799750334223}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.55141264078583, "4320.0": 329.42497918063066, "7560.0": 333.82602046942475, "21600.0": 323.68642879192487, "9720.0": 332.94208820576767, "16200.0": 325.82299128298814, "20520.0": 325.19753703643721, "3240.0": 329.66504941755875, "11880.0": 332.29546982118751, "14040.0": 326.51837436850099, "17280.0": 326.51851506890586, "2160.0": 323.70134945698589, "5400.0": 328.6805843225718, "18360.0": 324.79832692054578, "8640.0": 331.94068007914785, "6480.0": 332.75141896044545, "1080.0": 318.90722718736015, "15120.0": 325.85289150843209, "12960.0": 327.72250161440121, "19440.0": 325.17198606848808, "10800.0": 334.1255807822717}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out new file mode 100644 index 00000000000..627f92b1f83 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out @@ -0,0 +1 @@ +{"experiment": 9, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file From 07b612a1ef1396b831a35ad18d86faacc67a16d0 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 07:13:16 -0700 Subject: [PATCH 0427/1178] Removed group_data from parmest documentation. --- doc/OnlineDocs/contributed_packages/parmest/driver.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 28238928b83..e8d2fcd44e5 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -42,7 +42,6 @@ results, and fit distributions to theta values. .. autosummary:: :nosignatures: - ~pyomo.contrib.parmest.parmest.group_data ~pyomo.contrib.parmest.graphics.pairwise_plot ~pyomo.contrib.parmest.graphics.grouped_boxplot ~pyomo.contrib.parmest.graphics.grouped_violinplot From ff5077b202bcdccf8d4175d8e0afdcce6812aabd Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 08:34:36 -0700 Subject: [PATCH 0428/1178] Fixed parmest documentation to use examples with new UI. --- .../contributed_packages/parmest/datarec.rst | 13 ++++---- .../contributed_packages/parmest/driver.rst | 30 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index 6b721377e46..a3ece17190c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -38,9 +38,7 @@ is the response function that is defined in the model file): >>> import pandas as pd >>> import pyomo.contrib.parmest.parmest as parmest - >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model - - >>> theta_names = ['asymptote', 'rate_constant'] + >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], ... [4,16.0],[5,15.6],[7,19.8]], @@ -51,8 +49,13 @@ is the response function that is defined in the model file): ... - model.response_function[data.hour[i]])**2 for i in data.index) ... return expr - >>> pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE, - ... solver_options=None) + >>> def SSE(model): + ... expr = (model.experiment_outputs[model.y] + ... - model.response_function[model.experiment_outputs[model.hour]] + ... ) ** 2 + return expr + + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) >>> #print(var_values) diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index e8d2fcd44e5..45533e9520c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -57,21 +57,33 @@ Section. .. testsetup:: * :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available + # Data import pandas as pd - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model as model_function - data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], - [4,16.0],[5,15.6],[6,19.8]], - columns=['hour', 'y']) - theta_names = ['asymptote', 'rate_constant'] - def objective_function(model, data): - expr = sum((data.y[i] - model.response_function[data.hour[i]])**2 for i in data.index) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], + [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr + # Create an experiment list + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + .. doctest:: :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(model_function, data, theta_names, objective_function) + >>> pest = parmest.Estimator(exp_list, obj_function=SSE) Optionally, solver options can be supplied, e.g., @@ -79,7 +91,7 @@ Optionally, solver options can be supplied, e.g., :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available >>> solver_options = {"max_iter": 6000} - >>> pest = parmest.Estimator(model_function, data, theta_names, objective_function, solver_options) + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) From abbda5d91cf895e9342fd6891bc8508d1daa9921 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 09:03:10 -0700 Subject: [PATCH 0429/1178] Fixed parmest datarec documentation for new UI. --- .../contributed_packages/parmest/datarec.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index a3ece17190c..6e9be904286 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -40,20 +40,22 @@ is the response function that is defined in the model file): >>> import pyomo.contrib.parmest.parmest as parmest >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + >>> # Generate data >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], ... [4,16.0],[5,15.6],[7,19.8]], ... columns=['hour', 'y']) - >>> def SSE(model, data): - ... expr = sum((data.y[i]\ - ... - model.response_function[data.hour[i]])**2 for i in data.index) - ... return expr + >>> # Create an experiment list + >>> exp_list = [] + >>> for i in range(data.shape[0]): + ... exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + >>> # Define objective >>> def SSE(model): ... expr = (model.experiment_outputs[model.y] ... - model.response_function[model.experiment_outputs[model.hour]] ... ) ** 2 - return expr + ... return expr >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) From 54c3ab197a4aee657f5ea161991fd8277b04b033 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 08:49:07 -0700 Subject: [PATCH 0430/1178] Catch an edge case assigning new numeric types to Var/Param with units --- pyomo/core/base/param.py | 25 ++++++++++++++++++++----- pyomo/core/base/var.py | 15 ++++++++++----- pyomo/core/tests/unit/test_numvalue.py | 8 +++++++- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 03d700140e8..fc77c7b6f8f 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -162,16 +162,31 @@ def set_value(self, value, idx=NOTSET): # required to be mutable. # _comp = self.parent_component() - if type(value) in native_types: + if value.__class__ in native_types: # TODO: warn/error: check if this Param has units: assigning # a dimensionless value to a united param should be an error pass elif _comp._units is not None: _src_magnitude = expr_value(value) - _src_units = units.get_units(value) - value = units.convert_value( - num_value=_src_magnitude, from_units=_src_units, to_units=_comp._units - ) + # Note: expr_value() could have just registered a new numeric type + if value.__class__ in native_types: + value = _src_magnitude + else: + _src_units = units.get_units(value) + value = units.convert_value( + num_value=_src_magnitude, + from_units=_src_units, + to_units=_comp._units, + ) + # FIXME: we should call value() here [to ensure types get + # registered], but doing so breks non-numeric Params (which we + # allow). The real fix will be to follow the precedent from + # GetItemExpressiona and have separate types based on which + # expression "system" the Param should participate in (numeric, + # logical, or structural). + # + # else: + # value = expr_value(value) old_value, self._value = self._value, value try: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index d03fd0b677f..f426c9c4f55 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -384,17 +384,22 @@ def set_value(self, val, skip_validation=False): # # Check if this Var has units: assigning dimensionless # values to a variable with units should be an error - if type(val) not in native_numeric_types: - if self.parent_component()._units is not None: - _src_magnitude = value(val) + if val.__class__ in native_numeric_types: + pass + elif self.parent_component()._units is not None: + _src_magnitude = value(val) + # Note: value() could have just registered a new numeric type + if val.__class__ in native_numeric_types: + val = _src_magnitude + else: _src_units = units.get_units(val) val = units.convert_value( num_value=_src_magnitude, from_units=_src_units, to_units=self.parent_component()._units, ) - else: - val = value(val) + else: + val = value(val) if not skip_validation: if val not in self.domain: diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index eceab3a42d9..bd784d655e8 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -562,7 +562,8 @@ def test_numpy_basic_bool_registration(self): @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_automatic_numpy_registration(self): cmd = ( - 'import pyomo; from pyomo.core.base import Var, Param; import numpy as np; ' + 'import pyomo; from pyomo.core.base import Var, Param; ' + 'from pyomo.core.base.units_container import units; import numpy as np; ' 'print(np.float64 in pyomo.common.numeric_types.native_numeric_types); ' '%s; print(np.float64 in pyomo.common.numeric_types.native_numeric_types)' ) @@ -582,6 +583,11 @@ def _tester(expr): _tester('Var() + np.float64(5)') _tester('v = Var(); v.construct(); v.value = np.float64(5)') _tester('p = Param(mutable=True); p.construct(); p.value = np.float64(5)') + _tester('v = Var(units=units.m); v.construct(); v.value = np.float64(5)') + _tester( + 'p = Param(mutable=True, units=units.m); p.construct(); ' + 'p.value = np.float64(5)' + ) if __name__ == "__main__": From 0ad34438220112d6ac9213bd789fe679d50770f0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:12:47 -0700 Subject: [PATCH 0431/1178] Catch numpy.bool_ in the simple_constraint_rule decorator --- pyomo/core/base/constraint.py | 33 ++++++++++++++-------------- pyomo/core/base/indexed_component.py | 11 ++++++---- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index c67236656be..f3f0681d0fe 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -27,6 +27,7 @@ as_numeric, is_fixed, native_numeric_types, + native_logical_types, native_types, ) from pyomo.core.expr import ( @@ -84,14 +85,14 @@ def C_rule(model, i, j): model.c = Constraint(rule=simple_constraint_rule(...)) """ - return rule_wrapper( - rule, - { - None: Constraint.Skip, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + result_map = {None: Constraint.Skip} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types has the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=native_logical_types) def simple_constraintlist_rule(rule): @@ -109,14 +110,14 @@ def C_rule(model, i, j): model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ - return rule_wrapper( - rule, - { - None: ConstraintList.End, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + result_map = {None: ConstraintList.End} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types has the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=native_logical_types) # diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index abb29580960..0d498da091d 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -160,9 +160,12 @@ def _get_indexed_component_data_name(component, index): """ -def rule_result_substituter(result_map): +def rule_result_substituter(result_map, map_types): _map = result_map - _map_types = set(type(key) for key in result_map) + if map_types is None: + _map_types = set(type(key) for key in result_map) + else: + _map_types = map_types def rule_result_substituter_impl(rule, *args, **kwargs): if rule.__class__ in _map_types: @@ -203,7 +206,7 @@ def rule_result_substituter_impl(rule, *args, **kwargs): """ -def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): +def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None, map_types=None): """Wrap a rule with another function This utility method provides a way to wrap a function (rule) with @@ -230,7 +233,7 @@ def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): """ if isinstance(wrapping_fcn, dict): - wrapping_fcn = rule_result_substituter(wrapping_fcn) + wrapping_fcn = rule_result_substituter(wrapping_fcn, map_types) if not inspect.isfunction(rule): return wrapping_fcn(rule) # Because some of our processing of initializer functions relies on From 044f8476abfad0c4c3e1f3bc9d12235b22d80e62 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 10:21:51 -0700 Subject: [PATCH 0432/1178] Apply @blnicho 's csuggestions --- .../developer_reference/solvers.rst | 44 +++++++++++++++---- pyomo/__future__.py | 2 +- pyomo/contrib/solver/base.py | 2 +- pyomo/contrib/solver/config.py | 2 +- pyomo/contrib/solver/gurobi.py | 2 +- pyomo/contrib/solver/persistent.py | 10 ++--- pyomo/contrib/solver/sol_reader.py | 4 +- pyomo/contrib/solver/tests/unit/test_base.py | 8 ++-- 8 files changed, 51 insertions(+), 23 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 921e452004d..237bc7e523b 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -34,7 +34,7 @@ available are: Backwards Compatible Mode ^^^^^^^^^^^^^^^^^^^^^^^^^ -.. code-block:: python +.. testcode:: import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination @@ -52,13 +52,20 @@ Backwards Compatible Mode assert_optimal_termination(status) model.pprint() +.. testoutput:: + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + Future Capability Mode ^^^^^^^^^^^^^^^^^^^^^^ -There are multiple ways to utilize the future compatibility mode: direct import +There are multiple ways to utilize the future capability mode: direct import or changed ``SolverFactory`` version. -.. code-block:: python +.. testcode:: # Direct import import pyomo.environ as pyo @@ -81,9 +88,16 @@ or changed ``SolverFactory`` version. status.display() model.pprint() +.. testoutput:: + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + Changing the ``SolverFactory`` version: -.. code-block:: python +.. testcode:: # Change SolverFactory version import pyomo.environ as pyo @@ -105,6 +119,18 @@ Changing the ``SolverFactory`` version: status.display() model.pprint() +.. testoutput:: + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +.. testcode:: + :hide: + + from pyomo.__future__ import solver_factory_v1 + Linear Presolve and Scaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -118,11 +144,13 @@ options for certain solvers. Currently, these options are only available for The ``writer_config`` configuration option can be used to manipulate presolve and scaling options: -.. code-block:: python +.. testcode:: + + from pyomo.contrib.solver.ipopt import Ipopt + opt = Ipopt() + opt.config.writer_config.display() - >>> from pyomo.contrib.solver.ipopt import Ipopt - >>> opt = Ipopt() - >>> opt.config.writer_config.display() +.. testoutput:: show_section_timing: false skip_trivial_constraints: true diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 87b1d4e77b3..d298e12cab6 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -43,7 +43,7 @@ def solver_factory(version=None): This allows users to query / set the current implementation of the SolverFactory that should be used throughout Pyomo. Valid options are: - - ``1``: the original Pyomo SolverFactor + - ``1``: the original Pyomo SolverFactory - ``2``: the SolverFactory from APPSI - ``3``: the SolverFactory from pyomo.contrib.solver diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index cb13809c438..3bfa83050ad 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -288,7 +288,7 @@ def add_variables(self, variables: List[_GeneralVarData]): """ @abc.abstractmethod - def add_params(self, params: List[_ParamData]): + def add_parameters(self, params: List[_ParamData]): """ Add parameters to the model """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index ca9557d0002..0c86f7646d3 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -234,7 +234,7 @@ def __init__( default=True, description=""" If False, new/old parameters will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_params() and + solves. Use False only when manually updating the solver with opt.add_parameters() and opt.remove_params() or when you are certain parameters are not being added to / removed from the model.""", ), diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 85131ba73bd..ad476b9261e 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -475,7 +475,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_parameters(self, params: List[_ParamData]): pass def _reinit(self): diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 0994aa53093..e389e5d4019 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -75,13 +75,13 @@ def add_variables(self, variables: List[_GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): + def _add_parameters(self, params: List[_ParamData]): pass - def add_params(self, params: List[_ParamData]): + def add_parameters(self, params: List[_ParamData]): for p in params: self._params[id(p)] = p - self._add_params(params) + self._add_parameters(params) @abc.abstractmethod def _add_constraints(self, cons: List[_GeneralConstraintData]): @@ -191,7 +191,7 @@ def add_block(self, block): if p.mutable: for _p in p.values(): param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) + self.add_parameters(list(param_dict.values())) self.add_constraints( list( block.component_data_objects(Constraint, descend_into=True, active=True) @@ -403,7 +403,7 @@ def update(self, timer: HierarchicalTimer = None): if config.update_params: self.update_params() - self.add_params(new_params) + self.add_parameters(new_params) timer.stop('params') timer.start('vars') self.add_variables(new_vars) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 2817dab4516..41d840f8d07 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -36,7 +36,7 @@ def parse_sol_file( # # Some solvers (minto) do not write a message. We will assume - # all non-blank lines up the 'Options' line is the message. + # all non-blank lines up to the 'Options' line is the message. # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will # consider to be the solver message. @@ -168,7 +168,7 @@ def parse_sol_file( # The fourth entry is table "length", e.g., memory size. number_of_string_lines = int(line[5]) suffix_name = sol_file.readline().strip() - # Add any of arbitrary string lines to the "other" list + # Add any arbitrary string lines to the "other" list for line in range(number_of_string_lines): sol_data.other.append(sol_file.readline()) if data_type == 0: # Var diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 5fecd012cda..a9b3e4f4711 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -92,7 +92,7 @@ def test_abstract_member_list(self): 'remove_block', 'add_block', 'available', - 'add_params', + 'add_parameters', 'remove_constraints', 'add_variables', 'solve', @@ -110,7 +110,7 @@ def test_class_method_list(self): '_load_vars', 'add_block', 'add_constraints', - 'add_params', + 'add_parameters', 'add_variables', 'available', 'is_persistent', @@ -138,7 +138,7 @@ def test_init(self): self.assertTrue(self.instance.is_persistent()) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) - self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_parameters(None), None) self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) @@ -164,7 +164,7 @@ def test_context_manager(self): self.assertTrue(self.instance.is_persistent()) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) - self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_parameters(None), None) self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) From 6cb1180ed79aefbbf3dffb5623621cda05934785 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:42:25 -0700 Subject: [PATCH 0433/1178] NFC: updating comments --- pyomo/common/collections/component_map.py | 2 +- pyomo/common/collections/component_set.py | 1 + pyomo/common/collections/orderedset.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index c110e9f390b..caeabb1e650 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -80,7 +80,7 @@ class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): __autoslot_mappers__ = {'_dict': _rehash_keys} def __init__(self, *args, **kwds): - # maps id(obj) -> (obj,val) + # maps id_hash(obj) -> (obj,val) self._dict = {} # handle the dict-style initialization scenarios self.update(*args, **kwds) diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index 19d2ef2f7f9..5e9d794ff8e 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -63,6 +63,7 @@ class ComponentSet(AutoSlots.Mixin, collections_MutableSet): __autoslot_mappers__ = {'_data': _rehash_keys} def __init__(self, iterable=None): + # maps id_hash(obj) -> obj self._data = {} if iterable is not None: self.update(iterable) diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index 6bcf0c2fafb..834101e3896 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -18,8 +18,8 @@ class OrderedSet(AutoSlots.Mixin, MutableSet): __slots__ = ('_dict',) def __init__(self, iterable=None): - # TODO: Starting in Python 3.7, dict is ordered (and is faster - # than OrderedDict). dict began supporting reversed() in 3.8. + # Starting in Python 3.7, dict is ordered (and is faster than + # OrderedDict). dict began supporting reversed() in 3.8. self._dict = {} if iterable is not None: self.update(iterable) From f3ded2787e18757a2ec5ff0330c64acd338b45fd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:42:49 -0700 Subject: [PATCH 0434/1178] Clean up exception messages / string representation --- pyomo/common/collections/component_map.py | 8 ++++---- pyomo/common/collections/component_set.py | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index caeabb1e650..8dcfdb6c837 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -87,8 +87,8 @@ def __init__(self, *args, **kwds): def __str__(self): """String representation of the mapping.""" - tmp = {str(c) + " (id=" + str(id(c)) + ")": v for c, v in self.items()} - return "ComponentMap(" + str(tmp) + ")" + tmp = {f"{v[0]} (key={k})": v[1] for k, v in self._dict.items()} + return f"ComponentMap({tmp})" # # Implement MutableMapping abstract methods @@ -99,7 +99,7 @@ def __getitem__(self, obj): return self._dict[_hasher[obj.__class__](obj)][1] except KeyError: _id = _hasher[obj.__class__](obj) - raise KeyError("Component with id '%s': %s" % (_id, obj)) + raise KeyError(f"{obj} (key={_id})") from None def __setitem__(self, obj, val): self._dict[_hasher[obj.__class__](obj)] = (obj, val) @@ -109,7 +109,7 @@ def __delitem__(self, obj): del self._dict[_hasher[obj.__class__](obj)] except KeyError: _id = _hasher[obj.__class__](obj) - raise KeyError("Component with id '%s': %s" % (_id, obj)) + raise KeyError(f"{obj} (key={_id})") from None def __iter__(self): return (obj for obj, val in self._dict.values()) diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index 5e9d794ff8e..6e12bad7277 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -70,10 +70,8 @@ def __init__(self, iterable=None): def __str__(self): """String representation of the mapping.""" - tmp = [] - for objid, obj in self._data.items(): - tmp.append(str(obj) + " (id=" + str(objid) + ")") - return "ComponentSet(" + str(tmp) + ")" + tmp = [f"{v} (key={k})" for k, v in self._data.items()] + return f"ComponentSet({tmp})" def update(self, iterable): """Update a set with the union of itself and others.""" @@ -136,4 +134,4 @@ def remove(self, val): del self._data[_hasher[val.__class__](val)] except KeyError: _id = _hasher[val.__class__](val) - raise KeyError("Component with id '%s': %s" % (_id, val)) + raise KeyError(f"{val} (key={_id})") from None From c9a9f0d5132da90cecb43278dd2697d0389c2189 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:55:27 -0700 Subject: [PATCH 0435/1178] Ensure NoneType is in the map_types --- pyomo/core/base/constraint.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index f3f0681d0fe..108ff7383c3 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -85,6 +85,7 @@ def C_rule(model, i, j): model.c = Constraint(rule=simple_constraint_rule(...)) """ + map_types = set([type(None)]) | native_logical_types result_map = {None: Constraint.Skip} for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible @@ -92,7 +93,7 @@ def C_rule(model, i, j): # Note: some logical types has the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map - return rule_wrapper(rule, result_map, map_types=native_logical_types) + return rule_wrapper(rule, result_map, map_types=map_types) def simple_constraintlist_rule(rule): @@ -110,6 +111,7 @@ def C_rule(model, i, j): model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ + map_types = set([type(None)]) | native_logical_types result_map = {None: ConstraintList.End} for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible @@ -117,7 +119,7 @@ def C_rule(model, i, j): # Note: some logical types has the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map - return rule_wrapper(rule, result_map, map_types=native_logical_types) + return rule_wrapper(rule, result_map, map_types=map_types) # From 7ecdcc930c9f4d89777412da5915eadf78d70330 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:55:50 -0700 Subject: [PATCH 0436/1178] NFC: fix comment --- pyomo/core/base/constraint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 108ff7383c3..8cf3c48ad0a 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -90,7 +90,7 @@ def C_rule(model, i, j): for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible result_map[l_type(False)] = Constraint.Infeasible - # Note: some logical types has the same as bool (e.g., np.bool_), so + # Note: some logical types hash the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map return rule_wrapper(rule, result_map, map_types=map_types) @@ -116,7 +116,7 @@ def C_rule(model, i, j): for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible result_map[l_type(False)] = Constraint.Infeasible - # Note: some logical types has the same as bool (e.g., np.bool_), so + # Note: some logical types hash the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map return rule_wrapper(rule, result_map, map_types=map_types) From 423b1412fb4e2696b1a976a06793957505320dee Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 11:34:35 -0700 Subject: [PATCH 0437/1178] Add skip statement to doctests; start ipopt unit tests --- .../developer_reference/solvers.rst | 7 ++ pyomo/contrib/solver/tests/unit/test_ipopt.py | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 pyomo/contrib/solver/tests/unit/test_ipopt.py diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 237bc7e523b..45945c18b12 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -35,6 +35,7 @@ Backwards Compatible Mode ^^^^^^^^^^^^^^^^^^^^^^^^^ .. testcode:: + :skipif: not ipopt_available import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination @@ -53,6 +54,7 @@ Backwards Compatible Mode model.pprint() .. testoutput:: + :skipif: not ipopt_available :hide: 2 Var Declarations @@ -66,6 +68,7 @@ There are multiple ways to utilize the future capability mode: direct import or changed ``SolverFactory`` version. .. testcode:: + :skipif: not ipopt_available # Direct import import pyomo.environ as pyo @@ -89,6 +92,7 @@ or changed ``SolverFactory`` version. model.pprint() .. testoutput:: + :skipif: not ipopt_available :hide: solution_loader: ... @@ -98,6 +102,7 @@ or changed ``SolverFactory`` version. Changing the ``SolverFactory`` version: .. testcode:: + :skipif: not ipopt_available # Change SolverFactory version import pyomo.environ as pyo @@ -120,6 +125,7 @@ Changing the ``SolverFactory`` version: model.pprint() .. testoutput:: + :skipif: not ipopt_available :hide: solution_loader: ... @@ -127,6 +133,7 @@ Changing the ``SolverFactory`` version: 3 Declarations: x y obj .. testcode:: + :skipif: not ipopt_available :hide: from pyomo.__future__ import solver_factory_v1 diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py new file mode 100644 index 00000000000..2ddcce2e456 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -0,0 +1,73 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest, Executable +from pyomo.repn.plugins.nl_writer import NLWriter +from pyomo.contrib.solver import ipopt + + +ipopt_available = ipopt.Ipopt().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptSolverConfig(unittest.TestCase): + def test_default_instantiation(self): + config = ipopt.IpoptConfig() + # Should be inherited + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + # Unique to this object + self.assertIsInstance(config.executable, type(Executable('path'))) + self.assertIsInstance(config.writer_config, type(NLWriter.CONFIG())) + + def test_custom_instantiation(self): + config = ipopt.IpoptConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + # Default should be `ipopt` + self.assertIsNotNone(str(config.executable)) + self.assertIn('ipopt', str(config.executable)) + # Set to a totally bogus path + config.executable = Executable('/bogus/path') + self.assertIsNone(config.executable.executable) + self.assertFalse(config.executable.available()) + + +class TestIpoptResults(unittest.TestCase): + def test_default_instantiation(self): + res = ipopt.IpoptResults() + # Inherited methods/attributes + self.assertIsNone(res.solution_loader) + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.iteration_count) + self.assertIsNone(res.timing_info.start_timestamp) + self.assertIsNone(res.timing_info.wall_time) + # Unique to this object + self.assertIsNone(res.timing_info.ipopt_excluding_nlp_functions) + self.assertIsNone(res.timing_info.nlp_function_evaluations) + self.assertIsNone(res.timing_info.total_seconds) + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + pass From 924c38aebb82d2a72ba340df39f2948116fdc861 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 20 Feb 2024 11:43:00 -0700 Subject: [PATCH 0438/1178] NFC: Fixing typos in comments in param.py --- pyomo/core/base/param.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index fc77c7b6f8f..3ef33b9ee45 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -179,9 +179,9 @@ def set_value(self, value, idx=NOTSET): to_units=_comp._units, ) # FIXME: we should call value() here [to ensure types get - # registered], but doing so breks non-numeric Params (which we + # registered], but doing so breaks non-numeric Params (which we # allow). The real fix will be to follow the precedent from - # GetItemExpressiona and have separate types based on which + # GetItemExpression and have separate types based on which # expression "system" the Param should participate in (numeric, # logical, or structural). # From d0cdeaab066b35f164a9a05de01c580284559405 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 12:11:34 -0700 Subject: [PATCH 0439/1178] Consolidate log_solevr_output into tee --- pyomo/contrib/solver/config.py | 43 +++++++++++++++------ pyomo/contrib/solver/gurobi.py | 68 ++++++++++++++++------------------ pyomo/contrib/solver/ipopt.py | 7 +--- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 0c86f7646d3..7ca9ac104ae 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -9,6 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import io +import logging +import sys + +from collections.abc import Sequence from typing import Optional from pyomo.common.config import ( @@ -19,9 +24,29 @@ ADVANCED_OPTION, Bool, ) +from pyomo.common.log import LogStream +from pyomo.common.numeric_types import native_logical_types from pyomo.common.timing import HierarchicalTimer +def TextIO_or_Logger(val): + ans = [] + if not isinstance(val, Sequence): + val = [val] + for v in val: + if v.__class__ in native_logical_types: + if v: + ans.append(sys.stdout) + elif isinstance(v, io.TextIOBase): + ans.append(v) + elif isinstance(v, logging.Logger): + ans.append(LogStream(level=logging.INFO, logger=v)) + else: + raise ValueError( + "Expected bool, TextIOBase, or Logger, but received {v.__class__}" + ) + return ans + class SolverConfig(ConfigDict): """ Base config for all direct solver interfaces @@ -43,20 +68,16 @@ def __init__( visibility=visibility, ) - self.tee: bool = self.declare( + self.tee: List[TextIO] = self.declare( 'tee', ConfigValue( - domain=Bool, - default=False, - description="If True, the solver log prints to stdout.", - ), - ) - self.log_solver_output: bool = self.declare( - 'log_solver_output', - ConfigValue( - domain=Bool, + domain=TextIO_or_Logger, default=False, - description="If True, the solver output gets logged.", + description="""`tee` accepts :py:class:`bool`, + :py:class:`io.TextIOBase`, or :py:class:`logging.Logger` + (or a list of these types). ``True`` is mapped to + ``sys.stdout``. The solver log will be printed to each of + these streams / destinations. """, ), ) self.working_dir: Optional[str] = self.declare( diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index ad476b9261e..63387730c45 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -14,7 +14,6 @@ import math from typing import List, Optional from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet -from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException from pyomo.common.tee import capture_output, TeeStream @@ -326,42 +325,37 @@ def symbol_map(self): def _solve(self): config = self._config timer = config.timer - ostreams = [io.StringIO()] - if config.tee: - ostreams.append(sys.stdout) - if config.log_solver_output: - ostreams.append(LogStream(level=logging.INFO, logger=logger)) - - with TeeStream(*ostreams) as t: - with capture_output(output=t.STDOUT, capture_fd=False): - options = config.solver_options - - self._solver_model.setParam('LogToConsole', 1) - - if config.threads is not None: - self._solver_model.setParam('Threads', config.threads) - if config.time_limit is not None: - self._solver_model.setParam('TimeLimit', config.time_limit) - if config.rel_gap is not None: - self._solver_model.setParam('MIPGap', config.rel_gap) - if config.abs_gap is not None: - self._solver_model.setParam('MIPGapAbs', config.abs_gap) - - if config.use_mipstart: - for ( - pyomo_var_id, - gurobi_var, - ) in self._pyomo_var_to_solver_var_map.items(): - pyomo_var = self._vars[pyomo_var_id][0] - if pyomo_var.is_integer() and pyomo_var.value is not None: - self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) - - for key, option in options.items(): - self._solver_model.setParam(key, option) - - timer.start('optimize') - self._solver_model.optimize(self._callback) - timer.stop('optimize') + ostreams = [io.StringIO()] + config.tee + + with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): + options = config.solver_options + + self._solver_model.setParam('LogToConsole', 1) + + if config.threads is not None: + self._solver_model.setParam('Threads', config.threads) + if config.time_limit is not None: + self._solver_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + self._solver_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + for ( + pyomo_var_id, + gurobi_var, + ) in self._pyomo_var_to_solver_var_map.items(): + pyomo_var = self._vars[pyomo_var_id][0] + if pyomo_var.is_integer() and pyomo_var.value is not None: + self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) + + for key, option in options.items(): + self._solver_model.setParam(key, option) + + timer.start('optimize') + self._solver_model.optimize(self._callback) + timer.stop('optimize') self._needs_updated = False res = self._postsolve(timer) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 537e6f85968..38272d58fa1 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -36,7 +36,6 @@ from pyomo.contrib.solver.sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolSolutionLoader from pyomo.common.tee import TeeStream -from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix @@ -390,11 +389,7 @@ def solve(self, model, **kwds): else: timeout = None - ostreams = [io.StringIO()] - if config.tee: - ostreams.append(sys.stdout) - if config.log_solver_output: - ostreams.append(LogStream(level=logging.INFO, logger=logger)) + ostreams = [io.StringIO()] + config.tee with TeeStream(*ostreams) as t: timer.start('subprocess') process = subprocess.run( From ff92c62b89cc376ef87849adb35e5b0c741eed3a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 12:55:00 -0700 Subject: [PATCH 0440/1178] More unit tests for ipopt; fix some bugs as well --- pyomo/contrib/solver/ipopt.py | 49 ++--- pyomo/contrib/solver/tests/unit/test_ipopt.py | 172 +++++++++++++++++- 2 files changed, 199 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 537e6f85968..42ac24ec352 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -23,7 +23,7 @@ document_kwargs_from_configdict, ConfigDict, ) -from pyomo.common.errors import PyomoException +from pyomo.common.errors import PyomoException, DeveloperError from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.var import _GeneralVarData @@ -137,13 +137,18 @@ def get_reduced_costs( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) if len(self._nl_info.eliminated_vars) > 0: raise NotImplementedError( - 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.' + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) ' + 'to get dual variable values.' + ) + if self._sol_data is None: + raise DeveloperError( + "Solution data is empty. This should not " + "have happened. Report this error to the Pyomo Developers." ) - assert self._sol_data is not None if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) obj_scale = 1 @@ -252,7 +257,6 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None - self._executable = self.config.executable def available(self, config=None): if config is None: @@ -270,17 +274,20 @@ def version(self, config=None): config = self.config pth = config.executable.path() if self._version_cache is None or self._version_cache[0] != pth: - results = subprocess.run( - [str(pth), '--version'], - timeout=1, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - ) - version = results.stdout.splitlines()[0] - version = version.split(' ')[1].strip() - version = tuple(int(i) for i in version.split('.')) - self._version_cache = (pth, version) + if pth is None: + self._version_cache = (None, None) + else: + results = subprocess.run( + [str(pth), '--version'], + timeout=1, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1].strip() + version = tuple(int(i) for i in version.split('.')) + self._version_cache = (pth, version) return self._version_cache[1] def _write_options_file(self, filename: str, options: Mapping): @@ -292,15 +299,15 @@ def _write_options_file(self, filename: str, options: Mapping): # If it has options in it, parse them and write them to a file. # If they are command line options, ignore them; they will be # parsed during _create_command_line - with open(filename + '.opt', 'w') as opt_file: - for k, val in options.items(): - if k not in ipopt_command_line_options: - opt_file_exists = True + for k, val in options.items(): + if k not in ipopt_command_line_options: + opt_file_exists = True + with open(filename + '.opt', 'a+') as opt_file: opt_file.write(str(k) + ' ' + str(val) + '\n') return opt_file_exists def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): - cmd = [str(self._executable), basename + '.nl', '-AMPL'] + cmd = [str(config.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index 2ddcce2e456..ae07bd37f86 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -9,7 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import os + from pyomo.common import unittest, Executable +from pyomo.common.errors import DeveloperError +from pyomo.common.tempfiles import TempfileManager from pyomo.repn.plugins.nl_writer import NLWriter from pyomo.contrib.solver import ipopt @@ -68,6 +72,172 @@ def test_default_instantiation(self): self.assertIsNone(res.timing_info.total_seconds) +class TestIpoptSolutionLoader(unittest.TestCase): + def test_get_reduced_costs_error(self): + loader = ipopt.IpoptSolutionLoader(None, None) + with self.assertRaises(RuntimeError): + loader.get_reduced_costs() + + # Set _nl_info to something completely bogus but is not None + class NLInfo: + pass + + loader._nl_info = NLInfo() + loader._nl_info.eliminated_vars = [1, 2, 3] + with self.assertRaises(NotImplementedError): + loader.get_reduced_costs() + # Reset _nl_info so we can ensure we get an error + # when _sol_data is None + loader._nl_info.eliminated_vars = [] + with self.assertRaises(DeveloperError): + loader.get_reduced_costs() + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestIpoptInterface(unittest.TestCase): - pass + def test_class_member_list(self): + opt = ipopt.Ipopt() + expected_list = [ + 'Availability', + 'CONFIG', + 'config', + 'available', + 'is_persistent', + 'solve', + 'version', + 'name', + ] + method_list = [method for method in dir(opt) if method.startswith('_') is False] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_default_instantiation(self): + opt = ipopt.Ipopt() + self.assertFalse(opt.is_persistent()) + self.assertIsNotNone(opt.version()) + self.assertEqual(opt.name, 'ipopt') + self.assertEqual(opt.CONFIG, opt.config) + self.assertTrue(opt.available()) + + def test_context_manager(self): + with ipopt.Ipopt() as opt: + self.assertFalse(opt.is_persistent()) + self.assertIsNotNone(opt.version()) + self.assertEqual(opt.name, 'ipopt') + self.assertEqual(opt.CONFIG, opt.config) + self.assertTrue(opt.available()) + + def test_available_cache(self): + opt = ipopt.Ipopt() + opt.available() + self.assertTrue(opt._available_cache[1]) + self.assertIsNotNone(opt._available_cache[0]) + # Now we will try with a custom config that has a fake path + config = ipopt.IpoptConfig() + config.executable = Executable('/a/bogus/path') + opt.available(config=config) + self.assertFalse(opt._available_cache[1]) + self.assertIsNone(opt._available_cache[0]) + + def test_version_cache(self): + opt = ipopt.Ipopt() + opt.version() + self.assertIsNotNone(opt._version_cache[0]) + self.assertIsNotNone(opt._version_cache[1]) + # Now we will try with a custom config that has a fake path + config = ipopt.IpoptConfig() + config.executable = Executable('/a/bogus/path') + opt.version(config=config) + self.assertIsNone(opt._version_cache[0]) + self.assertIsNone(opt._version_cache[1]) + + def test_write_options_file(self): + # If we have no options, we should get false back + opt = ipopt.Ipopt() + result = opt._write_options_file('fakename', None) + self.assertFalse(result) + # Pass it some options that ARE on the command line + opt = ipopt.Ipopt(solver_options={'max_iter': 4}) + result = opt._write_options_file('myfile', opt.config.solver_options) + self.assertFalse(result) + self.assertFalse(os.path.isfile('myfile.opt')) + # Now we are going to actually pass it some options that are NOT on + # the command line + opt = ipopt.Ipopt(solver_options={'custom_option': 4}) + with TempfileManager.new_context() as temp: + dname = temp.mkdtemp() + if not os.path.exists(dname): + os.mkdir(dname) + filename = os.path.join(dname, 'myfile') + result = opt._write_options_file(filename, opt.config.solver_options) + self.assertTrue(result) + self.assertTrue(os.path.isfile(filename + '.opt')) + # Make sure all options are writing to the file + opt = ipopt.Ipopt(solver_options={'custom_option_1': 4, 'custom_option_2': 3}) + with TempfileManager.new_context() as temp: + dname = temp.mkdtemp() + if not os.path.exists(dname): + os.mkdir(dname) + filename = os.path.join(dname, 'myfile') + result = opt._write_options_file(filename, opt.config.solver_options) + self.assertTrue(result) + self.assertTrue(os.path.isfile(filename + '.opt')) + with open(filename + '.opt', 'r') as f: + data = f.readlines() + self.assertEqual(len(data), len(list(opt.config.solver_options.keys()))) + + def test_create_command_line(self): + opt = ipopt.Ipopt() + # No custom options, no file created. Plain and simple. + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual(result, [str(opt.config.executable), 'myfile.nl', '-AMPL']) + # Custom command line options + opt = ipopt.Ipopt(solver_options={'max_iter': 4}) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, [str(opt.config.executable), 'myfile.nl', '-AMPL', 'max_iter=4'] + ) + # Let's see if we correctly parse config.time_limit + opt = ipopt.Ipopt(solver_options={'max_iter': 4}, time_limit=10) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'max_iter=4', + 'max_cpu_time=10.0', + ], + ) + # Now let's do multiple command line options + opt = ipopt.Ipopt(solver_options={'max_iter': 4, 'max_cpu_time': 10}) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'max_cpu_time=10', + 'max_iter=4', + ], + ) + # Let's now include if we "have" an options file + result = opt._create_command_line('myfile', opt.config, True) + self.assertEqual( + result, + [ + '/Users/mmundt/Documents/idaes/venv-pyomo/bin/ipopt', + 'myfile.nl', + '-AMPL', + 'option_file_name=myfile.opt', + 'max_cpu_time=10', + 'max_iter=4', + ], + ) + # Finally, let's make sure it errors if someone tries to pass option_file_name + opt = ipopt.Ipopt( + solver_options={'max_iter': 4, 'option_file_name': 'myfile.opt'} + ) + with self.assertRaises(ValueError): + result = opt._create_command_line('myfile', opt.config, False) From c2472b3cb09cf367fb711845fb9780908c028f89 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 13:10:27 -0700 Subject: [PATCH 0441/1178] Remove Datetime validator; replace with IsInstance --- pyomo/common/config.py | 12 ------------ pyomo/common/tests/test_config.py | 17 ----------------- pyomo/contrib/solver/config.py | 1 + pyomo/contrib/solver/results.py | 5 +++-- 4 files changed, 4 insertions(+), 31 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 46a05494094..238bdd78e9d 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -18,7 +18,6 @@ import argparse import builtins -import datetime import enum import importlib import inspect @@ -205,16 +204,6 @@ def NonNegativeFloat(val): return ans -def Datetime(val): - """Domain validation function to check for datetime.datetime type. - - This domain will return the original object, assuming it is of the right type. - """ - if not isinstance(val, datetime.datetime): - raise ValueError(f"Expected datetime object, but received {type(val)}.") - return val - - class In(object): """In(domain, cast=None) Domain validation class admitting a Container of possible values @@ -794,7 +783,6 @@ def from_enum_or_string(cls, arg): NegativeFloat NonPositiveFloat NonNegativeFloat - Datetime In InEnum IsInstance diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index e2f64a3a9d5..02f4fc88251 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -48,7 +48,6 @@ def yaml_load(arg): ConfigDict, ConfigValue, ConfigList, - Datetime, MarkImmutable, ImmutableConfigValue, Bool, @@ -937,22 +936,6 @@ def _rule(key, val): } ) - def test_Datetime(self): - c = ConfigDict() - c.declare('a', ConfigValue(domain=Datetime, default=None)) - self.assertEqual(c.get('a').domain_name(), 'Datetime') - - self.assertEqual(c.a, None) - c.a = datetime.datetime(2022, 1, 1) - self.assertEqual(c.a, datetime.datetime(2022, 1, 1)) - - with self.assertRaises(ValueError): - c.a = 5 - with self.assertRaises(ValueError): - c.a = 'Hello' - with self.assertRaises(ValueError): - c.a = False - class TestImmutableConfigValue(unittest.TestCase): def test_immutable_config_value(self): diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 7ca9ac104ae..8f715ac7250 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -47,6 +47,7 @@ def TextIO_or_Logger(val): ) return ans + class SolverConfig(ConfigDict): """ Base config for all direct solver interfaces diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 88de0624629..699137d2fc9 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -16,7 +16,7 @@ from pyomo.common.config import ( ConfigDict, ConfigValue, - Datetime, + IsInstance, NonNegativeInt, In, NonNegativeFloat, @@ -262,7 +262,8 @@ def __init__( self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue( - domain=Datetime, description="UTC timestamp of when run was initiated." + domain=IsInstance(datetime), + description="UTC timestamp of when run was initiated.", ), ) self.timing_info.wall_time: Optional[float] = self.timing_info.declare( From d1549d69c21c754b9ffe2c0a2f60d5aafc2e70a6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:21:50 -0700 Subject: [PATCH 0442/1178] Not checking isinstance when we check ctype in util --- pyomo/gdp/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 0e4e5f5e9ff..57eef29eded 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -169,7 +169,7 @@ def parent_disjunct(self, u): Arg: u : A node in the forest """ - if isinstance(u, _DisjunctData) or u.ctype is Disjunct: + if u.ctype is Disjunct: return self.parent(self.parent(u)) else: return self.parent(u) @@ -186,7 +186,7 @@ def root_disjunct(self, u): while True: if parent is None: return rootmost_disjunct - if isinstance(parent, _DisjunctData) or parent.ctype is Disjunct: + if parent.ctype is Disjunct: rootmost_disjunct = parent parent = self.parent(parent) @@ -246,7 +246,7 @@ def leaves(self): @property def disjunct_nodes(self): for v in self._vertices: - if isinstance(v, _DisjunctData) or v.ctype is Disjunct: + if v.ctype is Disjunct: yield v From 06621a75a2151076d90ee7b9a91b9794fdbac29b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:22:45 -0700 Subject: [PATCH 0443/1178] Not checking isinstance when we check ctype in hull --- pyomo/gdp/plugins/hull.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index a70aa2760eb..b2e1ffd76fd 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -212,7 +212,7 @@ def _get_user_defined_local_vars(self, targets): # we cache what Blocks/Disjuncts we've already looked on so that we # don't duplicate effort. for t in targets: - if t.ctype is Disjunct or isinstance(t, _DisjunctData): + if t.ctype is Disjunct: # first look beneath where we are (there could be Blocks on this # disjunct) for b in t.component_data_objects( From 3435aa1a82fcfec68227faba0918f0629680e699 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:24:16 -0700 Subject: [PATCH 0444/1178] Prettier defaultdicts --- pyomo/gdp/plugins/hull.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b2e1ffd76fd..5a3349a8b34 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -206,7 +206,7 @@ def _collect_local_vars_from_block(self, block, local_var_dict): local_var_dict[disj].update(var_list) def _get_user_defined_local_vars(self, targets): - user_defined_local_vars = defaultdict(lambda: ComponentSet()) + user_defined_local_vars = defaultdict(ComponentSet) seen_blocks = set() # we go through the targets looking both up and down the hierarchy, but # we cache what Blocks/Disjuncts we've already looked on so that we @@ -369,7 +369,7 @@ def _transform_disjunctionData( # actually appear in any Constraints on that Disjunct, but in order to # do this, we will explicitly collect the set of local_vars in this # loop. - local_vars = defaultdict(lambda: ComponentSet()) + local_vars = defaultdict(ComponentSet) for var in var_order: disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct From b673bf74c0664e33679a8998ffc2a2ce72cb3d3e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:32:03 -0700 Subject: [PATCH 0445/1178] stopping the Suffix search going up once we hit a seen block --- pyomo/gdp/plugins/hull.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 5a3349a8b34..911233a0b2b 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -227,11 +227,12 @@ def _get_user_defined_local_vars(self, targets): # now look up in the tree blk = t while blk is not None: - if blk not in seen_blocks: - self._collect_local_vars_from_block( - blk, user_defined_local_vars - ) - seen_blocks.add(blk) + if blk in seen_blocks: + break + self._collect_local_vars_from_block( + blk, user_defined_local_vars + ) + seen_blocks.add(blk) blk = blk.parent_block() return user_defined_local_vars From 044316c796f11a8e2bf77b8ca797d95a7cb4fcf4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:59:13 -0700 Subject: [PATCH 0446/1178] Black --- pyomo/gdp/plugins/hull.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 911233a0b2b..6ee329cbff7 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -229,9 +229,7 @@ def _get_user_defined_local_vars(self, targets): while blk is not None: if blk in seen_blocks: break - self._collect_local_vars_from_block( - blk, user_defined_local_vars - ) + self._collect_local_vars_from_block(blk, user_defined_local_vars) seen_blocks.add(blk) blk = blk.parent_block() return user_defined_local_vars From 074ea7807b226c8854c74d15a6d433955dd32da5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 14:01:10 -0700 Subject: [PATCH 0447/1178] Generalize the tests --- pyomo/contrib/solver/tests/unit/test_ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index ae07bd37f86..eff8787592e 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -227,7 +227,7 @@ def test_create_command_line(self): self.assertEqual( result, [ - '/Users/mmundt/Documents/idaes/venv-pyomo/bin/ipopt', + str(opt.config.executable), 'myfile.nl', '-AMPL', 'option_file_name=myfile.opt', From 66285d9d9ff544a03ad3c176f6018b0858d9abad Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 14:35:25 -0700 Subject: [PATCH 0448/1178] Switching the bigm constraint map to a DefaultComponentMap :) --- pyomo/gdp/plugins/hull.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 6ee329cbff7..f1ed574907c 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -15,7 +15,7 @@ import pyomo.common.config as cfg from pyomo.common import deprecated -from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numvalue import ZeroConstant import pyomo.core.expr as EXPR @@ -457,11 +457,9 @@ def _transform_disjunctionData( dis_var_info['original_var_map'] = ComponentMap() original_var_map = dis_var_info['original_var_map'] if 'bigm_constraint_map' not in dis_var_info: - dis_var_info['bigm_constraint_map'] = ComponentMap() + dis_var_info['bigm_constraint_map'] = DefaultComponentMap(dict) bigm_constraint_map = dis_var_info['bigm_constraint_map'] - if disaggregated_var not in bigm_constraint_map: - bigm_constraint_map[disaggregated_var] = {} bigm_constraint_map[disaggregated_var][obj] = Reference( disaggregated_var_bounds[idx, :] ) @@ -565,9 +563,7 @@ def _transform_disjunct( # update the bigm constraint mappings data_dict = disaggregatedVar.parent_block().private_data() if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = ComponentMap() - if disaggregatedVar not in data_dict['bigm_constraint_map']: - data_dict['bigm_constraint_map'][disaggregatedVar] = {} + data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) data_dict['bigm_constraint_map'][disaggregatedVar][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = disaggregatedVar @@ -598,9 +594,7 @@ def _transform_disjunct( # update the bigm constraint mappings data_dict = var.parent_block().private_data() if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = ComponentMap() - if var not in data_dict['bigm_constraint_map']: - data_dict['bigm_constraint_map'][var] = {} + data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) data_dict['bigm_constraint_map'][var][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = var From b96bd2a583f894d38a963d5f133404e551aea1e7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 15:05:17 -0700 Subject: [PATCH 0449/1178] Add Block.register_private_data_initializer() --- pyomo/core/base/block.py | 27 ++++++++++++-- pyomo/core/tests/unit/test_block.py | 58 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 48353078fca..a0948c693d7 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -14,13 +14,13 @@ import sys import weakref import textwrap -from contextlib import contextmanager +from collections import defaultdict +from contextlib import contextmanager from inspect import isclass, currentframe +from io import StringIO from itertools import filterfalse, chain from operator import itemgetter, attrgetter -from io import StringIO -from pyomo.common.pyomo_typing import overload from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import Mapping @@ -28,6 +28,7 @@ from pyomo.common.formatting import StreamIndenter from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set +from pyomo.common.pyomo_typing import overload from pyomo.common.timing import ConstructionTimer from pyomo.core.base.component import ( Component, @@ -1986,7 +1987,7 @@ def private_data(self, scope=None): if self._private_data is None: self._private_data = {} if scope not in self._private_data: - self._private_data[scope] = {} + self._private_data[scope] = Block._private_data_initializers[scope]() return self._private_data[scope] @@ -2004,6 +2005,7 @@ class Block(ActiveIndexedComponent): """ _ComponentDataClass = _BlockData + _private_data_initializers = defaultdict(lambda: dict) def __new__(cls, *args, **kwds): if cls != Block: @@ -2207,6 +2209,23 @@ def display(self, filename=None, ostream=None, prefix=""): for key in sorted(self): _BlockData.display(self[key], filename, ostream, prefix) + @staticmethod + def register_private_data_initializer(initializer, scope=None): + mod = currentframe().f_back.f_globals['__name__'] + if scope is None: + scope = mod + elif not mod.startswith(scope): + raise ValueError( + "'private_data' scope must be substrings of the caller's module name. " + f"Received '{scope}' when calling register_private_data_initializer()." + ) + if scope in Block._private_data_initializers: + raise RuntimeError( + "Duplicate initializer registration for 'private_data' dictionary " + f"(scope={scope})" + ) + Block._private_data_initializers[scope] = initializer + class ScalarBlock(_BlockData, Block): def __init__(self, *args, **kwds): diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index c9c68a820f7..88646643703 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3437,6 +3437,64 @@ def test_private_data(self): mfe4 = m.b.b[1].private_data('pyomo.core.tests') self.assertIs(mfe4, mfe3) + def test_register_private_data(self): + _save = Block._private_data_initializers + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + def init(): + return {'a': None, 'b': 1} + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {'a': None, 'b': 1}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + Block.register_private_data_initializer(init, 'pyomo') + self.assertEqual(len(pdi), 2) + + with self.assertRaisesRegex( + RuntimeError, + r"Duplicate initializer registration for 'private_data' " + r"dictionary \(scope=pyomo.core.tests.unit.test_block\)", + ): + Block.register_private_data_initializer(init) + + with self.assertRaisesRegex( + ValueError, + r"'private_data' scope must be substrings of the caller's " + r"module name. Received 'invalid' when calling " + r"register_private_data_initializer\(\).", + ): + Block.register_private_data_initializer(init, 'invalid') + + self.assertEqual(len(pdi), 2) + finally: + Block._private_data_initializers = _save + if __name__ == "__main__": unittest.main() From 05e0b470731611dc50725c1128613c1db3d84f58 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 16:08:45 -0700 Subject: [PATCH 0450/1178] Bug fix: missing imports --- pyomo/contrib/solver/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 8f715ac7250..c91eb603b32 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -14,7 +14,7 @@ import sys from collections.abc import Sequence -from typing import Optional +from typing import Optional, List, TextIO from pyomo.common.config import ( ConfigDict, From a462f362a0dd4214ee28a7f9a4d829a6e90d8419 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 16:34:09 -0700 Subject: [PATCH 0451/1178] Registering a data class with the private data for the hull scope--this is very pretty --- pyomo/gdp/plugins/hull.py | 103 ++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 60 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index f1ed574907c..78d4e917fca 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -13,6 +13,7 @@ from collections import defaultdict +from pyomo.common.autoslots import AutoSlots import pyomo.common.config as cfg from pyomo.common import deprecated from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap @@ -55,6 +56,17 @@ logger = logging.getLogger('pyomo.gdp.hull') +class _HullTransformationData(AutoSlots.Mixin): + __slots__ = ('disaggregated_var_map', + 'original_var_map', + 'bigm_constraint_map') + + def __init__(self): + self.disaggregated_var_map = DefaultComponentMap(ComponentMap) + self.original_var_map = ComponentMap() + self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + +Block.register_private_data_initializer(_HullTransformationData) @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." @@ -449,21 +461,13 @@ def _transform_disjunctionData( ) # Update mappings: var_info = var.parent_block().private_data() - if 'disaggregated_var_map' not in var_info: - var_info['disaggregated_var_map'] = ComponentMap() - disaggregated_var_map = var_info['disaggregated_var_map'] + disaggregated_var_map = var_info.disaggregated_var_map dis_var_info = disaggregated_var.parent_block().private_data() - if 'original_var_map' not in dis_var_info: - dis_var_info['original_var_map'] = ComponentMap() - original_var_map = dis_var_info['original_var_map'] - if 'bigm_constraint_map' not in dis_var_info: - dis_var_info['bigm_constraint_map'] = DefaultComponentMap(dict) - bigm_constraint_map = dis_var_info['bigm_constraint_map'] - - bigm_constraint_map[disaggregated_var][obj] = Reference( + + dis_var_info.bigm_constraint_map[disaggregated_var][obj] = Reference( disaggregated_var_bounds[idx, :] ) - original_var_map[disaggregated_var] = var + dis_var_info.original_var_map[disaggregated_var] = var # For every Disjunct the Var does not appear in, we want to map # that this new variable is its disaggreggated variable. @@ -475,8 +479,6 @@ def _transform_disjunctionData( disj._transformation_block is not None and disj not in disjuncts_var_appears_in[var] ): - if not disj in disaggregated_var_map: - disaggregated_var_map[disj] = ComponentMap() disaggregated_var_map[disj][var] = disaggregated_var # start the expression for the reaggregation constraint with @@ -562,9 +564,7 @@ def _transform_disjunct( ) # update the bigm constraint mappings data_dict = disaggregatedVar.parent_block().private_data() - if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) - data_dict['bigm_constraint_map'][disaggregatedVar][obj] = bigmConstraint + data_dict.bigm_constraint_map[disaggregatedVar][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = disaggregatedVar for var in local_vars: @@ -593,9 +593,7 @@ def _transform_disjunct( ) # update the bigm constraint mappings data_dict = var.parent_block().private_data() - if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) - data_dict['bigm_constraint_map'][var][obj] = bigmConstraint + data_dict.bigm_constraint_map[var][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = var var_substitute_map = dict( @@ -645,21 +643,13 @@ def _declare_disaggregated_var_bounds( bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) original_var_info = original_var.parent_block().private_data() - if 'disaggregated_var_map' not in original_var_info: - original_var_info['disaggregated_var_map'] = ComponentMap() - disaggregated_var_map = original_var_info['disaggregated_var_map'] - + disaggregated_var_map = original_var_info.disaggregated_var_map disaggregated_var_info = disaggregatedVar.parent_block().private_data() - if 'original_var_map' not in disaggregated_var_info: - disaggregated_var_info['original_var_map'] = ComponentMap() - original_var_map = disaggregated_var_info['original_var_map'] # store the mappings from variables to their disaggregated selves on # the transformation block - if disjunct not in disaggregated_var_map: - disaggregated_var_map[disjunct] = ComponentMap() disaggregated_var_map[disjunct][original_var] = disaggregatedVar - original_var_map[disaggregatedVar] = original_var + disaggregated_var_info.original_var_map[disaggregatedVar] = original_var def _get_local_var_list(self, parent_disjunct): # Add or retrieve Suffix from parent_disjunct so that, if this is @@ -885,17 +875,12 @@ def get_disaggregated_var(self, v, disjunct, raise_exception=True): "It does not appear '%s' is a " "variable that appears in disjunct '%s'" % (v.name, disjunct.name) ) - var_map = v.parent_block().private_data() - if 'disaggregated_var_map' in var_map: - try: - return var_map['disaggregated_var_map'][disjunct][v] - except: - if raise_exception: - logger.error(msg) - raise - elif raise_exception: - raise GDP_Error(msg) - return None + disaggregated_var_map = v.parent_block().private_data().disaggregated_var_map + if v in disaggregated_var_map[disjunct]: + return disaggregated_var_map[disjunct][v] + else: + if raise_exception: + raise GDP_Error(msg) def get_src_var(self, disaggregated_var): """ @@ -910,9 +895,8 @@ def get_src_var(self, disaggregated_var): of some Disjunct) """ var_map = disaggregated_var.parent_block().private_data() - if 'original_var_map' in var_map: - if disaggregated_var in var_map['original_var_map']: - return var_map['original_var_map'][disaggregated_var] + if disaggregated_var in var_map.original_var_map: + return var_map.original_var_map[disaggregated_var] raise GDP_Error( "'%s' does not appear to be a " "disaggregated variable" % disaggregated_var.name @@ -979,22 +963,21 @@ def get_var_bounds_constraint(self, v, disjunct=None): Optional since for non-nested models this can be inferred. """ info = v.parent_block().private_data() - if 'bigm_constraint_map' in info: - if v in info['bigm_constraint_map']: - if len(info['bigm_constraint_map'][v]) == 1: - # Not nested, or it's at the top layer, so we're fine. - return list(info['bigm_constraint_map'][v].values())[0] - elif disjunct is not None: - # This is nested, so we need to walk up to find the active ones - return info['bigm_constraint_map'][v][disjunct] - else: - raise ValueError( - "It appears that the variable '%s' appears " - "within a nested GDP hierarchy, and no " - "'disjunct' argument was specified. Please " - "specify for which Disjunct the bounds " - "constraint for '%s' should be returned." % (v, v) - ) + if v in info.bigm_constraint_map: + if len(info.bigm_constraint_map[v]) == 1: + # Not nested, or it's at the top layer, so we're fine. + return list(info.bigm_constraint_map[v].values())[0] + elif disjunct is not None: + # This is nested, so we need to walk up to find the active ones + return info.bigm_constraint_map[v][disjunct] + else: + raise ValueError( + "It appears that the variable '%s' appears " + "within a nested GDP hierarchy, and no " + "'disjunct' argument was specified. Please " + "specify for which Disjunct the bounds " + "constraint for '%s' should be returned." % (v, v) + ) raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " From 5a71219cb49d236719b8a9c725ac37ac6a7c020f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 16:35:22 -0700 Subject: [PATCH 0452/1178] Black is relatively tame --- pyomo/gdp/plugins/hull.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 78d4e917fca..d1c38bde039 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -56,18 +56,19 @@ logger = logging.getLogger('pyomo.gdp.hull') + class _HullTransformationData(AutoSlots.Mixin): - __slots__ = ('disaggregated_var_map', - 'original_var_map', - 'bigm_constraint_map') + __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map') def __init__(self): self.disaggregated_var_map = DefaultComponentMap(ComponentMap) self.original_var_map = ComponentMap() self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + Block.register_private_data_initializer(_HullTransformationData) + @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." ) From 33a05453c2251d0a51821ea1b8733fb11f57a7c1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 23:42:21 -0700 Subject: [PATCH 0453/1178] Update intersphinx links, remove documentation of nonfunctional code --- doc/OnlineDocs/conf.py | 4 ++-- .../library_reference/expressions/context_managers.rst | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 89c346f5abc..7196606b7d6 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -57,8 +57,8 @@ 'numpy': ('https://numpy.org/doc/stable/', None), 'pandas': ('https://pandas.pydata.org/docs/', None), 'scikit-learn': ('https://scikit-learn.org/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'Sphinx': ('https://www.sphinx-doc.org/en/stable/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + 'Sphinx': ('https://www.sphinx-doc.org/en/master/', None), } # -- General configuration ------------------------------------------------ diff --git a/doc/OnlineDocs/library_reference/expressions/context_managers.rst b/doc/OnlineDocs/library_reference/expressions/context_managers.rst index 0e92f583c73..ae6884d684f 100644 --- a/doc/OnlineDocs/library_reference/expressions/context_managers.rst +++ b/doc/OnlineDocs/library_reference/expressions/context_managers.rst @@ -8,6 +8,3 @@ Context Managers .. autoclass:: pyomo.core.expr.linear_expression :members: -.. autoclass:: pyomo.core.expr.current.clone_counter - :members: - From 90f6901c51ca5272d5ccfd1ce6787bfdce1ad499 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 01:30:42 -0700 Subject: [PATCH 0454/1178] Updating CHANGELOG in preparation for the release --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553a4f1c3bd..747025a8bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,81 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.1 (21 Feb 2024) +------------------------------------------------------------------------------- + +- General + - Add support for tuples in `ComponentMap`; add `DefaultComponentMap` (#3150) + - Update `Path`, `PathList`, and `IsInstance` Domain Validators (#3144) + - Remove usage of `__all__` (#3142) + - Extend Path and Type Checking Validators of `common.config` (#3140) + - Update Copyright Statements (#3139) + - Update `ExitNodeDispatcher` to better support extensibility (#3125) + - Create contributors data gathering script (#3117) + - Prevent duplicate entries in ConfigDict declaration order (#3116) + - Remove unnecessary `__future__` imports (#3109) + - Import pandas through pyomo.common.dependencies (#3102) + - Update links to workshop slides (#3079) + - Remove incorrect use of identity (is) comparisons (#3061) +- Core + - Add `Block.register_private_data_initializer()` (#3153) + - Generalize the simple_constraint_rule decorator (#3152) + - Fix edge case assigning new numeric types to Var/Param with units (#3151) + - Add private_data to `_BlockData` (#3138) + - Convert implicit sets created by `IndexedComponent`s to "anonymous" sets (#3075) + - Add `all_different` and `count_if` to the logical expression system (#3058) + - Fix RangeSet.__len__ when defined by floats (#3119) + - Overhaul the `Suffix` component (#3072) + - Enforce expression immutability in `expr.args` (#3099) + - Improve NumPy registration when assigning numpy to Param (#3093) + - Track changes in PyPy behavior introduced in 7.3.14 (#3087) + - Remove automatic numpy import (#3077) + - Fix `range_difference` for Sets with nonzero anchor points (#3063) + - Clarify errors raised by accessing Sets by positional index (#3062) +- Documentation + - Update MPC documentation and citation (#3148) + - Fix an error in the documentation for LinearExpression (#3090) + - Fix bugs in the documentation of Pyomo.DoE (#3070) + - Fix a latex_printer vestige in the documentation (#3066) +- Solver Interfaces + - Make error msg more explicit wrt different interfaces (#3141) + - NLv2: only raise exception for empty models in the legacy API (#3135) + - Add `to_expr()` to AMPLRepn, fix NLWriterInfo return type (#3095) +- Testing + - Update Release Wheel Builder Action (#3149) + - Actions Version Update: Address node.js deprecations (#3118) + - New Black Major Release (24.1.0) (#3108) + - Use scip for PyROS tests (#3104) + - Add missing solver dependency flags for OnlineDocs tests (#3094) + - Re-enable `contrib.viewer.tests.test_qt.py` (#3085) + - Add automated testing of OnlineDocs examples (#3080) + - Silence deprecation warnings emitted by Pyomo tests (#3076) + - Fix Python 3.12 tests (manage `pyutilib`, `distutils` dependencies) (#3065) +- DAE + - Replace deprecated `numpy.math` alias with standard `math` module (#3074) +- GDP + - Handle nested GDPs correctly in all the transformations (#3145) + - Fix bugs in nested models in gdp.hull transformation (#3143) + - Various bug fixes in gdp.mbigm transformation (#3073) + - Add GDP => MINLP Transformation (#3082) +- Contributed Packages + - GDPopt: Fix lbb solve_data bug (#3133) + - GDPopt: Adding missing import for gdpopt.enumerate (#3105) + - FBBT: Extend `fbbt.ExpressionBoundsVisitor` to handle relational + expressions and Expr_if (#3129) + - incidence_analysis: Method to add an edge in IncidenceGraphInterface (#3120) + - incidence_analysis: Add subgraph method to IncidencegraphInterface (#3122) + - incidence_analysis: Add `ampl_repn` option (#3069) + - incidence_analysis: Fix config documentation of `linear_only` argument in + `get_incident_variables` (#3067) + - interior_point: Workaround for improvement in Mumps memory prediction + algorithm (#3114) + - MindtPy: Various bug fixes (#3034) + - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) + - PyROS: Update Subproblem Initialization Routines (#3071) + - PyROS: Fix DR polishing under nominal objective focus (#3060) + ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) ------------------------------------------------------------------------------- From 1bccb1699bd504af1e851eb8ffdb61e152af6578 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 21 Feb 2024 08:03:57 -0700 Subject: [PATCH 0455/1178] Address comments from @jsiirola, @blnicho --- pyomo/common/tests/test_config.py | 1 - pyomo/contrib/appsi/base.py | 1 + pyomo/contrib/solver/base.py | 12 ++- pyomo/contrib/solver/config.py | 22 +++--- pyomo/contrib/solver/gurobi.py | 4 +- pyomo/contrib/solver/ipopt.py | 79 ++++--------------- pyomo/contrib/solver/persistent.py | 16 ++-- .../tests/solvers/test_gurobi_persistent.py | 2 +- .../solver/tests/solvers/test_solvers.py | 31 +++----- pyomo/contrib/solver/tests/unit/test_base.py | 16 ++-- pyomo/contrib/solver/tests/unit/test_ipopt.py | 20 +---- .../contrib/solver/tests/unit/test_results.py | 6 +- 12 files changed, 68 insertions(+), 142 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 02f4fc88251..0bbed43423d 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -25,7 +25,6 @@ # ___________________________________________________________________________ import argparse -import datetime import enum import os import os.path diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..d028bdc4fde 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -597,6 +597,7 @@ def __init__( class Solver(abc.ABC): class Availability(enum.IntEnum): + """Docstring""" NotFound = 0 BadVersion = -1 BadLicense = -2 diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 3bfa83050ad..f3d60bef03d 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -125,7 +125,7 @@ def solve(self, model: _BlockData, **kwargs) -> Results: """ @abc.abstractmethod - def available(self): + def available(self) -> bool: """Test if the solver is available on this system. Nominally, this will return True if the solver interface is @@ -159,7 +159,7 @@ def version(self) -> Tuple: A tuple representing the version """ - def is_persistent(self): + def is_persistent(self) -> bool: """ Returns ------- @@ -178,9 +178,7 @@ class PersistentSolverBase(SolverBase): Example usage can be seen in the Gurobi interface. """ - CONFIG = PersistentSolverConfig() - - @document_kwargs_from_configdict(CONFIG) + @document_kwargs_from_configdict(PersistentSolverConfig()) @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: super().solve(model, kwargs) @@ -312,7 +310,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): """ @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): + def remove_parameters(self, params: List[_ParamData]): """ Remove parameters from the model """ @@ -336,7 +334,7 @@ def update_variables(self, variables: List[_GeneralVarData]): """ @abc.abstractmethod - def update_params(self): + def update_parameters(self): """ Update parameters on the model """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index c91eb603b32..e60219a74b5 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -23,6 +23,7 @@ NonNegativeInt, ADVANCED_OPTION, Bool, + Path, ) from pyomo.common.log import LogStream from pyomo.common.numeric_types import native_logical_types @@ -74,17 +75,17 @@ def __init__( ConfigValue( domain=TextIO_or_Logger, default=False, - description="""`tee` accepts :py:class:`bool`, + description="""``tee`` accepts :py:class:`bool`, :py:class:`io.TextIOBase`, or :py:class:`logging.Logger` (or a list of these types). ``True`` is mapped to ``sys.stdout``. The solver log will be printed to each of - these streams / destinations. """, + these streams / destinations.""", ), ) - self.working_dir: Optional[str] = self.declare( + self.working_dir: Optional[Path] = self.declare( 'working_dir', ConfigValue( - domain=str, + domain=Path(), default=None, description="The directory in which generated files should be saved. " "This replaces the `keepfiles` option.", @@ -134,7 +135,8 @@ def __init__( self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue( - domain=NonNegativeFloat, description="Time limit applied to the solver." + domain=NonNegativeFloat, + description="Time limit applied to the solver (in seconds).", ), ) self.solver_options: ConfigDict = self.declare( @@ -201,7 +203,7 @@ class AutoUpdateConfig(ConfigDict): check_for_new_objective: bool update_constraints: bool update_vars: bool - update_params: bool + update_parameters: bool update_named_expressions: bool update_objective: bool treat_fixed_vars_as_params: bool @@ -257,7 +259,7 @@ def __init__( description=""" If False, new/old parameters will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_parameters() and - opt.remove_params() or when you are certain parameters are not being added to / + opt.remove_parameters() or when you are certain parameters are not being added to / removed from the model.""", ), ) @@ -297,15 +299,15 @@ def __init__( opt.update_variables() or when you are certain variables are not being modified.""", ), ) - self.update_params: bool = self.declare( - 'update_params', + self.update_parameters: bool = self.declare( + 'update_parameters', ConfigValue( domain=bool, default=True, description=""" If False, changes to parameter values will not be automatically detected on subsequent solves. Use False only when manually updating the solver with - opt.update_params() or when you are certain parameters are not being modified.""", + opt.update_parameters() or when you are certain parameters are not being modified.""", ), ) self.update_named_expressions: bool = self.declare( diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 63387730c45..d0ac0d80f45 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -747,7 +747,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[_ParamData]): pass def _update_variables(self, variables: List[_GeneralVarData]): @@ -770,7 +770,7 @@ def _update_variables(self, variables: List[_GeneralVarData]): gurobipy_var.setAttr('vtype', vtype) self._needs_updated = True - def update_params(self): + def update_parameters(self): for con, helpers in self._mutable_helpers.items(): for helper in helpers: helper.update() diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 3e911aea036..ad12e26ee92 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -13,16 +13,10 @@ import subprocess import datetime import io -import sys from typing import Mapping, Optional, Sequence from pyomo.common import Executable -from pyomo.common.config import ( - ConfigValue, - NonNegativeFloat, - document_kwargs_from_configdict, - ConfigDict, -) +from pyomo.common.config import ConfigValue, document_kwargs_from_configdict, ConfigDict from pyomo.common.errors import PyomoException, DeveloperError from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer @@ -78,54 +72,7 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', NLWriter.CONFIG() - ) - - -class IpoptResults(Results): - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - self.timing_info.ipopt_excluding_nlp_functions: Optional[float] = ( - self.timing_info.declare( - 'ipopt_excluding_nlp_functions', - ConfigValue( - domain=NonNegativeFloat, - default=None, - description="Total CPU seconds in IPOPT without function evaluations.", - ), - ) - ) - self.timing_info.nlp_function_evaluations: Optional[float] = ( - self.timing_info.declare( - 'nlp_function_evaluations', - ConfigValue( - domain=NonNegativeFloat, - default=None, - description="Total CPU seconds in NLP function evaluations.", - ), - ) - ) - self.timing_info.total_seconds: Optional[float] = self.timing_info.declare( - 'total_seconds', - ConfigValue( - domain=NonNegativeFloat, - default=None, - description="Total seconds in IPOPT. NOTE: Newer versions of IPOPT (3.14+) " - "no longer separate timing information.", - ), + 'writer_config', ConfigValue(default=NLWriter.CONFIG(), description="Configuration that controls options in the NL writer.") ) @@ -416,11 +363,11 @@ def solve(self, model, **kwds): if len(nl_info.variables) == 0: if len(nl_info.eliminated_vars) == 0: - results = IpoptResults() + results = Results() results.termination_condition = TerminationCondition.emptyModel results.solution_loader = SolSolutionLoader(None, None) else: - results = IpoptResults() + results = Results() results.termination_condition = ( TerminationCondition.convergenceCriteriaSatisfied ) @@ -435,18 +382,22 @@ def solve(self, model, **kwds): results = self._parse_solution(sol_file, nl_info) timer.stop('parse_sol') else: - results = IpoptResults() + results = Results() if process.returncode != 0: results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error results.solution_loader = SolSolutionLoader(None, None) else: results.iteration_count = iters - results.timing_info.ipopt_excluding_nlp_functions = ( - ipopt_time_nofunc - ) - results.timing_info.nlp_function_evaluations = ipopt_time_func - results.timing_info.total_seconds = ipopt_total_time + if ipopt_time_nofunc is not None: + results.timing_info.ipopt_excluding_nlp_functions = ( + ipopt_time_nofunc + ) + + if ipopt_time_func is not None: + results.timing_info.nlp_function_evaluations = ipopt_time_func + if ipopt_total_time is not None: + results.timing_info.total_seconds = ipopt_total_time if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -554,7 +505,7 @@ def _parse_ipopt_output(self, stream: io.StringIO): return iters, nofunc_time, func_time, total_time def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): - results = IpoptResults() + results = Results() res, sol_data = parse_sol_file( sol_file=instream, nl_info=nl_info, result=results ) diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index e389e5d4019..4b1a7c58dcd 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -274,11 +274,11 @@ def remove_variables(self, variables: List[_GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[_ParamData]): pass - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) + def remove_parameters(self, params: List[_ParamData]): + self._remove_parameters(params) for p in params: del self._params[id(p)] @@ -297,7 +297,7 @@ def remove_block(self, block): ) ) ) - self.remove_params( + self.remove_parameters( list( dict( (id(p), p) @@ -325,7 +325,7 @@ def update_variables(self, variables: List[_GeneralVarData]): self._update_variables(variables) @abc.abstractmethod - def update_params(self): + def update_parameters(self): pass def update(self, timer: HierarchicalTimer = None): @@ -396,12 +396,12 @@ def update(self, timer: HierarchicalTimer = None): self.remove_sos_constraints(old_sos) timer.stop('cons') timer.start('params') - self.remove_params(old_params) + self.remove_parameters(old_params) # sticking this between removal and addition # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() + if config.update_parameters: + self.update_parameters() self.add_parameters(new_params) timer.stop('params') diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py index f2dd79619b4..2f281e2abf0 100644 --- a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -487,7 +487,7 @@ def setUp(self): opt.config.auto_updates.check_for_new_or_removed_params = False opt.config.auto_updates.check_for_new_or_removed_vars = False opt.config.auto_updates.check_for_new_or_removed_constraints = False - opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_parameters = False opt.config.auto_updates.update_vars = False opt.config.auto_updates.update_constraints = False opt.config.auto_updates.update_named_expressions = False diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index c6c73ea2dc7..cf5f6cf5c57 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -9,23 +9,24 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import random +import math +from typing import Type + import pyomo.environ as pe +from pyomo import gdp from pyomo.common.dependencies import attempt_import import pyomo.common.unittest as unittest - -parameterized, param_available = attempt_import('parameterized') -parameterized = parameterized.parameterized from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.ipopt import Ipopt from pyomo.contrib.solver.gurobi import Gurobi -from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression -import math -numpy, numpy_available = attempt_import('numpy') -import random -from pyomo import gdp + +np, numpy_available = attempt_import('numpy') +parameterized, param_available = attempt_import('parameterized') +parameterized = parameterized.parameterized if not param_available: @@ -802,10 +803,6 @@ def test_mutable_param_with_range( opt.config.writer_config.linear_presolve = True else: opt.config.writer_config.linear_presolve = False - try: - import numpy as np - except: - raise unittest.SkipTest('numpy is not available') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -907,7 +904,7 @@ def test_add_and_remove_vars( m.y = pe.Var(bounds=(-1, None)) m.obj = pe.Objective(expr=m.y) if opt.is_persistent(): - opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_parameters = False opt.config.auto_updates.update_vars = False opt.config.auto_updates.update_constraints = False opt.config.auto_updates.update_named_expressions = False @@ -1003,14 +1000,10 @@ def test_with_numpy( a2 = -2 b2 = 1 m.c1 = pe.Constraint( - expr=(numpy.float64(0), m.y - numpy.int64(1) * m.x - numpy.float32(3), None) + expr=(np.float64(0), m.y - np.int64(1) * m.x - np.float32(3), None) ) m.c2 = pe.Constraint( - expr=( - None, - -m.y + numpy.int32(-2) * m.x + numpy.float64(1), - numpy.float16(0), - ) + expr=(None, -m.y + np.int32(-2) * m.x + np.float64(1), np.float16(0)) ) res = opt.solve(m) self.assertEqual(res.solution_status, SolutionStatus.optimal) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index a9b3e4f4711..74c495b86cc 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -80,7 +80,7 @@ def test_custom_solver_name(self): class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = [ - 'remove_params', + 'remove_parameters', 'version', 'update_variables', 'remove_variables', @@ -88,7 +88,7 @@ def test_abstract_member_list(self): '_get_primals', 'set_instance', 'set_objective', - 'update_params', + 'update_parameters', 'remove_block', 'add_block', 'available', @@ -116,12 +116,12 @@ def test_class_method_list(self): 'is_persistent', 'remove_block', 'remove_constraints', - 'remove_params', + 'remove_parameters', 'remove_variables', 'set_instance', 'set_objective', 'solve', - 'update_params', + 'update_parameters', 'update_variables', 'version', ] @@ -142,12 +142,12 @@ def test_init(self): self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) - self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_parameters(None), None) self.assertEqual(self.instance.remove_constraints(None), None) self.assertEqual(self.instance.remove_block(None), None) self.assertEqual(self.instance.set_objective(None), None) self.assertEqual(self.instance.update_variables(None), None) - self.assertEqual(self.instance.update_params(), None) + self.assertEqual(self.instance.update_parameters(), None) with self.assertRaises(NotImplementedError): self.instance._get_primals() @@ -168,12 +168,12 @@ def test_context_manager(self): self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) - self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_parameters(None), None) self.assertEqual(self.instance.remove_constraints(None), None) self.assertEqual(self.instance.remove_block(None), None) self.assertEqual(self.instance.set_objective(None), None) self.assertEqual(self.instance.update_variables(None), None) - self.assertEqual(self.instance.update_params(), None) + self.assertEqual(self.instance.update_parameters(), None) class TestLegacySolverWrapper(unittest.TestCase): diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index eff8787592e..cc459245506 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -44,7 +44,7 @@ def test_custom_instantiation(self): config.tee = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") - self.assertFalse(config.time_limit) + self.assertIsNone(config.time_limit) # Default should be `ipopt` self.assertIsNotNone(str(config.executable)) self.assertIn('ipopt', str(config.executable)) @@ -54,24 +54,6 @@ def test_custom_instantiation(self): self.assertFalse(config.executable.available()) -class TestIpoptResults(unittest.TestCase): - def test_default_instantiation(self): - res = ipopt.IpoptResults() - # Inherited methods/attributes - self.assertIsNone(res.solution_loader) - self.assertIsNone(res.incumbent_objective) - self.assertIsNone(res.objective_bound) - self.assertIsNone(res.solver_name) - self.assertIsNone(res.solver_version) - self.assertIsNone(res.iteration_count) - self.assertIsNone(res.timing_info.start_timestamp) - self.assertIsNone(res.timing_info.wall_time) - # Unique to this object - self.assertIsNone(res.timing_info.ipopt_excluding_nlp_functions) - self.assertIsNone(res.timing_info.nlp_function_evaluations) - self.assertIsNone(res.timing_info.total_seconds) - - class TestIpoptSolutionLoader(unittest.TestCase): def test_get_reduced_costs_error(self): loader = ipopt.IpoptSolutionLoader(None, None) diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 4856b737295..74404aaba4c 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -21,7 +21,7 @@ from pyomo.contrib.solver import results from pyomo.contrib.solver import solution import pyomo.environ as pyo -from pyomo.core.base.var import ScalarVar +from pyomo.core.base.var import Var class SolutionLoaderExample(solution.SolutionLoaderBase): @@ -213,8 +213,8 @@ def test_display(self): def test_generated_results(self): m = pyo.ConcreteModel() - m.x = ScalarVar() - m.y = ScalarVar() + m.x = Var() + m.y = Var() m.c1 = pyo.Constraint(expr=m.x == 1) m.c2 = pyo.Constraint(expr=m.y == 2) From 9b21273f92d4b62d3666f9ee1dbcb61741376b65 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 21 Feb 2024 08:08:41 -0700 Subject: [PATCH 0456/1178] Apply black --- pyomo/contrib/appsi/base.py | 1 - pyomo/contrib/solver/ipopt.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index d028bdc4fde..201e5975ac9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -597,7 +597,6 @@ def __init__( class Solver(abc.ABC): class Availability(enum.IntEnum): - """Docstring""" NotFound = 0 BadVersion = -1 BadLicense = -2 diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index ad12e26ee92..3ac1a5ac4a2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -72,7 +72,11 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', ConfigValue(default=NLWriter.CONFIG(), description="Configuration that controls options in the NL writer.") + 'writer_config', + ConfigValue( + default=NLWriter.CONFIG(), + description="Configuration that controls options in the NL writer.", + ), ) From 434c1dadeeed7531a6b372661891dbf17b7d647b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:11:46 -0700 Subject: [PATCH 0457/1178] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 747025a8bdf..c548a8c830c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Pyomo 6.7.1 (21 Feb 2024) - Fix `range_difference` for Sets with nonzero anchor points (#3063) - Clarify errors raised by accessing Sets by positional index (#3062) - Documentation + - Update intersphinx links, remove docs for nonfunctional code (#3155) - Update MPC documentation and citation (#3148) - Fix an error in the documentation for LinearExpression (#3090) - Fix bugs in the documentation of Pyomo.DoE (#3070) From 92163f2989dc99d4e14f63c658c0cc77e3d9dc7a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:15:41 -0700 Subject: [PATCH 0458/1178] cleanup --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/__init__.py | 11 +++++++++++ pyomo/contrib/simplification/build.py | 11 +++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 83e652cbef8..57c24d99090 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -157,7 +157,7 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install ginac + - name: install GiNaC if: matrix.other == '/singletest' run: | cd .. diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 6df28fbadc9..a55d100a18f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -179,7 +179,7 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install ginac + - name: install GiNaC if: matrix.other == '/singletest' run: | cd .. diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index 3abe5a25ba0..b4fa68eb386 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .simplify import Simplifier diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 39742e1e351..67ae1d37335 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.fileutils import this_file_dir, find_library import os From 0dff80fb37b9052805d744f42236711067d69bcc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:16:59 -0700 Subject: [PATCH 0459/1178] cleanup --- pyomo/contrib/simplification/build.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 67ae1d37335..4bf28a0fa33 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -9,15 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.fileutils import this_file_dir, find_library +import glob import os -from distutils.dist import Distribution -import sys import shutil -import glob +import sys import tempfile +from distutils.dist import Distribution + +from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.envvar import PYOMO_CONFIG_DIR +from pyomo.common.fileutils import find_library, this_file_dir def build_ginac_interface(args=[]): From c49c2df810775f1c64702a26e3cf75c36f717c99 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:23:10 -0700 Subject: [PATCH 0460/1178] cleanup --- pyomo/contrib/simplification/build.py | 11 ++++++----- pyomo/contrib/simplification/ginac_interface.cpp | 11 +++++++++++ pyomo/contrib/simplification/simplify.py | 11 +++++++++++ pyomo/contrib/simplification/tests/__init__.py | 11 +++++++++++ .../simplification/tests/test_simplification.py | 11 +++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 4bf28a0fa33..d9d1e701290 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -21,7 +21,9 @@ from pyomo.common.fileutils import find_library, this_file_dir -def build_ginac_interface(args=[]): +def build_ginac_interface(args=None): + if args is None: + args = list() dname = this_file_dir() _sources = ['ginac_interface.cpp'] sources = list() @@ -29,7 +31,6 @@ def build_ginac_interface(args=[]): sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') - print(ginac_lib) if ginac_lib is None: raise RuntimeError( 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' @@ -62,7 +63,7 @@ def build_ginac_interface(args=[]): extra_compile_args=extra_args, ) - class ginac_build_ext(build_ext): + class ginacBuildExt(build_ext): def run(self): basedir = os.path.abspath(os.path.curdir) if self.inplace: @@ -72,7 +73,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super(ginac_build_ext, self).run() + super(ginacBuildExt, self).run() if not self.inplace: library = glob.glob("build/*/ginac_interface.*")[0] target = os.path.join( @@ -94,7 +95,7 @@ def run(self): 'name': 'ginac_interface', 'packages': [], 'ext_modules': [ext], - 'cmdclass': {"build_ext": ginac_build_ext}, + 'cmdclass': {"build_ext": ginacBuildExt}, } dist = Distribution(package_config) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 32bea8dadd0..489f281bc2c 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,3 +1,14 @@ +// ___________________________________________________________________________ +// +// Pyomo: Python Optimization Modeling Objects +// Copyright (c) 2008-2022 +// National Technology and Engineering Solutions of Sandia, LLC +// Under the terms of Contract DE-NA0003525 with National Technology and +// Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +// rights in this software. +// This software is distributed under the 3-clause BSD License. +// ___________________________________________________________________________ + #include "ginac_interface.hpp" diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 4002f1a233f..b8cc4995f91 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index e69de29bb2d..9320e403e95 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index e3c60cb02ca..95402f98318 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.unittest import TestCase from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier From 29d6a19d0f1704a294d62d5a89370d6110a4fbe7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:29:35 -0700 Subject: [PATCH 0461/1178] cleanup --- pyomo/contrib/simplification/tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - From 3e5cca27025a04d09ecf938433a9afb3534d501b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:30:29 -0700 Subject: [PATCH 0462/1178] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c548a8c830c..daba7cac96c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ Pyomo 6.7.1 (21 Feb 2024) - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) - PyROS: Update Subproblem Initialization Routines (#3071) - PyROS: Fix DR polishing under nominal objective focus (#3060) + - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137) ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) From d027b190e1331db30b40bf25c2ff4b4a0fccd621 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:35:49 -0700 Subject: [PATCH 0463/1178] Updating deprecation version --- pyomo/contrib/solver/base.py | 2 +- pyomo/core/base/suffix.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index f3d60bef03d..13bd5ddb212 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -396,7 +396,7 @@ def _map_config( "`keepfiles` has been deprecated in the new solver interface. " "Use `working_dir` instead to designate a directory in which " f"files should be generated and saved. Setting `working_dir` to `{cwd}`.", - version='6.7.1.dev0', + version='6.7.1', ) self.config.working_dir = cwd # I believe this currently does nothing; however, it is unclear what diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 0c27eee060f..be2f732650d 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -341,7 +341,7 @@ def clear_all_values(self): @deprecated( 'Suffix.set_datatype is replaced with the Suffix.datatype property', - version='6.7.1.dev0', + version='6.7.1', ) def set_datatype(self, datatype): """ @@ -351,7 +351,7 @@ def set_datatype(self, datatype): @deprecated( 'Suffix.get_datatype is replaced with the Suffix.datatype property', - version='6.7.1.dev0', + version='6.7.1', ) def get_datatype(self): """ @@ -361,7 +361,7 @@ def get_datatype(self): @deprecated( 'Suffix.set_direction is replaced with the Suffix.direction property', - version='6.7.1.dev0', + version='6.7.1', ) def set_direction(self, direction): """ @@ -371,7 +371,7 @@ def set_direction(self, direction): @deprecated( 'Suffix.get_direction is replaced with the Suffix.direction property', - version='6.7.1.dev0', + version='6.7.1', ) def get_direction(self): """ From 156bf168ce817ad3a9c942b94535bf2876baae64 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:36:26 -0700 Subject: [PATCH 0464/1178] Updating RELEASE.md --- RELEASE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE.md b/RELEASE.md index 03baa803ac9..8313c969f25 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,6 +11,7 @@ The following are highlights of the 6.7 release series: - New writer for converting linear models to matrix form - New packages: - latex_printer (print Pyomo models to a LaTeX compatible format) + - contrib.solve: Part 1 of refactoring Pyomo's solver interfaces - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the From a4d109425a4e7f1f271aeddf0fd536c909f6c9fc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:36:46 -0700 Subject: [PATCH 0465/1178] Clarify some solver documentation --- .../developer_reference/solvers.rst | 113 +++++++++++++----- 1 file changed, 83 insertions(+), 30 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 45945c18b12..9f18119e373 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -1,10 +1,11 @@ Future Solver Interface Changes =============================== -Pyomo offers interfaces into multiple solvers, both commercial and open source. -To support better capabilities for solver interfaces, the Pyomo team is actively -redesigning the existing interfaces to make them more maintainable and intuitive -for use. Redesigned interfaces can be found in ``pyomo.contrib.solver``. +Pyomo offers interfaces into multiple solvers, both commercial and open +source. To support better capabilities for solver interfaces, the Pyomo +team is actively redesigning the existing interfaces to make them more +maintainable and intuitive for use. A preview of the redesigned +interfaces can be found in ``pyomo.contrib.solver``. .. currentmodule:: pyomo.contrib.solver @@ -12,27 +13,39 @@ for use. Redesigned interfaces can be found in ``pyomo.contrib.solver``. New Interface Usage ------------------- -The new interfaces have two modes: backwards compatible and future capability. -The future capability mode can be accessed directly or by switching the default -``SolverFactory`` version (see :doc:`future`). Currently, the new versions -available are: +The new interfaces are not completely backwards compatible with the +existing Pyomo solver interfaces. However, to aid in testing and +evaluation, we are distributing versions of the new solver interfaces +that are compatible with the existing ("legacy") solver interface. +These "legacy" interfaces are registered with the current +``SolverFactory`` using slightly different names (to avoid conflicts +with existing interfaces). -.. list-table:: Available Redesigned Solvers - :widths: 25 25 25 +.. |br| raw:: html + +
+ +.. list-table:: Available Redesigned Solvers and Names Registered + in the SolverFactories :header-rows: 1 * - Solver - - ``SolverFactory`` (v1) Name - - ``SolverFactory`` (v3) Name - * - ipopt - - ``ipopt_v2`` + - Name registered in the |br| ``pyomo.contrib.solver.factory.SolverFactory`` + - Name registered in the |br| ``pyomo.opt.base.solvers.LegacySolverFactory`` + * - Ipopt - ``ipopt`` + - ``ipopt_v2`` * - Gurobi - - ``gurobi_v2`` - ``gurobi`` + - ``gurobi_v2`` -Backwards Compatible Mode -^^^^^^^^^^^^^^^^^^^^^^^^^ +Using the new interfaces through the legacy interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface as exposed through the existing (legacy) +solver factory and solver interface wrapper. This provides an API that +is compatible with the existing (legacy) Pyomo solver interface and can +be used with other Pyomo tools / capabilities. .. testcode:: :skipif: not ipopt_available @@ -61,11 +74,10 @@ Backwards Compatible Mode ... 3 Declarations: x y obj -Future Capability Mode -^^^^^^^^^^^^^^^^^^^^^^ +Using the new interfaces directly +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -There are multiple ways to utilize the future capability mode: direct import -or changed ``SolverFactory`` version. +Here we use the new interface by importing it directly: .. testcode:: :skipif: not ipopt_available @@ -87,7 +99,7 @@ or changed ``SolverFactory`` version. opt = Ipopt() status = opt.solve(model) assert_optimal_termination(status) - # Displays important results information; only available in future capability mode + # Displays important results information; only available through the new interfaces status.display() model.pprint() @@ -99,7 +111,49 @@ or changed ``SolverFactory`` version. ... 3 Declarations: x y obj -Changing the ``SolverFactory`` version: +Using the new interfaces through the "new" SolverFactory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface by retrieving it from the new ``SolverFactory``: + +.. testcode:: + :skipif: not ipopt_available + + # Direct import + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.factory import SolverFactory + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = SolverFactory('ipopt') + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +Switching all of Pyomo to use the new interfaces +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We also provide a mechansim to get a "preview" of the future where we +replace the existing (legacy) SolverFactory and utilities with the new +(development) version: .. testcode:: :skipif: not ipopt_available @@ -120,7 +174,7 @@ Changing the ``SolverFactory`` version: status = pyo.SolverFactory('ipopt').solve(model) assert_optimal_termination(status) - # Displays important results information; only available in future capability mode + # Displays important results information; only available through the new interfaces status.display() model.pprint() @@ -141,16 +195,15 @@ Changing the ``SolverFactory`` version: Linear Presolve and Scaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The new interface will allow for direct manipulation of linear presolve and scaling -options for certain solvers. Currently, these options are only available for -``ipopt``. +The new interface allows access to new capabilities in the various +problem writers, including the linear presolve and scaling options +recently incorporated into the redesigned NL writer. For example, you +can control the NL writer in the new ``ipopt`` interface through the +solver's ``writer_config`` configuration option: .. autoclass:: pyomo.contrib.solver.ipopt.Ipopt :members: solve -The ``writer_config`` configuration option can be used to manipulate presolve -and scaling options: - .. testcode:: from pyomo.contrib.solver.ipopt import Ipopt From 286e99de1e076c19f74df6705d1b8493cfb82992 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:42:55 -0700 Subject: [PATCH 0466/1178] Fix typos --- doc/OnlineDocs/developer_reference/solvers.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 9f18119e373..cdf36b74397 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -82,7 +82,7 @@ Here we use the new interface by importing it directly: .. testcode:: :skipif: not ipopt_available - # Direct import + # Direct import import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.contrib.solver.ipopt import Ipopt @@ -119,7 +119,7 @@ Here we use the new interface by retrieving it from the new ``SolverFactory``: .. testcode:: :skipif: not ipopt_available - # Direct import + # Import through new SolverFactory import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.contrib.solver.factory import SolverFactory @@ -151,14 +151,14 @@ Here we use the new interface by retrieving it from the new ``SolverFactory``: Switching all of Pyomo to use the new interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We also provide a mechansim to get a "preview" of the future where we +We also provide a mechanism to get a "preview" of the future where we replace the existing (legacy) SolverFactory and utilities with the new (development) version: .. testcode:: :skipif: not ipopt_available - # Change SolverFactory version + # Change default SolverFactory version import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.__future__ import solver_factory_v3 From 93fff175609a32262728a33a16e27866398b5f62 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:45:34 -0700 Subject: [PATCH 0467/1178] Restore link to future docs --- doc/OnlineDocs/developer_reference/solvers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index cdf36b74397..7b17c4b40f0 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -153,7 +153,7 @@ Switching all of Pyomo to use the new interfaces We also provide a mechanism to get a "preview" of the future where we replace the existing (legacy) SolverFactory and utilities with the new -(development) version: +(development) version (see :doc:`future`): .. testcode:: :skipif: not ipopt_available From a906f9fb760d64bf96d60aa0093ddafacefd14d7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:52:29 -0700 Subject: [PATCH 0468/1178] Fix incorrect docstring --- pyomo/contrib/solver/results.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 699137d2fc9..cbc04681235 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -164,10 +164,12 @@ class Results(ConfigDict): iteration_count: int The total number of iterations. timing_info: ConfigDict - A ConfigDict containing two pieces of information: - start_timestamp: UTC timestamp of when run was initiated - wall_time: elapsed wall clock time for entire process - timer: a HierarchicalTimer object containing timing data about the solve + A ConfigDict containing three pieces of information: + - ``start_timestamp``: UTC timestamp of when run was initiated + - ``wall_time``: elapsed wall clock time for entire process + - ``timer``: a HierarchicalTimer object containing timing data about the solve + + Specific solvers may add other relevant timing information, as appropriate. extra_info: ConfigDict A ConfigDict to store extra information such as solver messages. solver_configuration: ConfigDict From cf5dc9c954700d106152ff6afc47a766a4062001 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:55:39 -0700 Subject: [PATCH 0469/1178] Add warning / link to #1030 --- doc/OnlineDocs/developer_reference/solvers.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 7b17c4b40f0..6168da3480e 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -1,6 +1,16 @@ Future Solver Interface Changes =============================== +.. note:: + + The new solver interfaces are still under active development. They + are included in the releases as development previews. Please be + aware that APIs and functionality may change with no notice. + + We welcome any feedback and ideas as we develop this capability. + Please post feedback on + `Issue 1030 `_. + Pyomo offers interfaces into multiple solvers, both commercial and open source. To support better capabilities for solver interfaces, the Pyomo team is actively redesigning the existing interfaces to make them more From 63cc14d28a4b15552bb2e8d82eae5dcc75bbac2b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 10:02:09 -0700 Subject: [PATCH 0470/1178] More edits to the CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daba7cac96c..faa2fa094f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ Pyomo 6.7.1 (21 Feb 2024) - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) - PyROS: Update Subproblem Initialization Routines (#3071) - PyROS: Fix DR polishing under nominal objective focus (#3060) - - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137) + - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137, #3156) ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) From 83040cdfd08a26aab94966a50b1e5e4d16cc62fa Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 10:10:41 -0700 Subject: [PATCH 0471/1178] More updates to the CHANGELOG --- CHANGELOG.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faa2fa094f8..c06e0f71378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Pyomo 6.7.1 (21 Feb 2024) - Generalize the simple_constraint_rule decorator (#3152) - Fix edge case assigning new numeric types to Var/Param with units (#3151) - Add private_data to `_BlockData` (#3138) - - Convert implicit sets created by `IndexedComponent`s to "anonymous" sets (#3075) + - IndexComponent create implicit sets as "anonymous" sets (#3075) - Add `all_different` and `count_if` to the logical expression system (#3058) - Fix RangeSet.__len__ when defined by floats (#3119) - Overhaul the `Suffix` component (#3072) @@ -38,9 +38,11 @@ Pyomo 6.7.1 (21 Feb 2024) - Update intersphinx links, remove docs for nonfunctional code (#3155) - Update MPC documentation and citation (#3148) - Fix an error in the documentation for LinearExpression (#3090) - - Fix bugs in the documentation of Pyomo.DoE (#3070) - - Fix a latex_printer vestige in the documentation (#3066) + - Fix Pyomo.DoE documentation (#3070) + - Fix latex_printer documentation (#3066) - Solver Interfaces + - Preview release of new solver interfaces as pyomo.contrib.solver + (#3137, #3156) - Make error msg more explicit wrt different interfaces (#3141) - NLv2: only raise exception for empty models in the legacy API (#3135) - Add `to_expr()` to AMPLRepn, fix NLWriterInfo return type (#3095) @@ -69,15 +71,12 @@ Pyomo 6.7.1 (21 Feb 2024) - incidence_analysis: Method to add an edge in IncidenceGraphInterface (#3120) - incidence_analysis: Add subgraph method to IncidencegraphInterface (#3122) - incidence_analysis: Add `ampl_repn` option (#3069) - - incidence_analysis: Fix config documentation of `linear_only` argument in - `get_incident_variables` (#3067) - - interior_point: Workaround for improvement in Mumps memory prediction - algorithm (#3114) + - incidence_analysis: Update documentation (#3067) + - interior_point: Resolve test failure due to Mumps update (#3114) - MindtPy: Various bug fixes (#3034) - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) - PyROS: Update Subproblem Initialization Routines (#3071) - PyROS: Fix DR polishing under nominal objective focus (#3060) - - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137, #3156) ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) From 7b7f3881103a0333453417b268f87a259d1eeeec Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 21 Feb 2024 10:21:32 -0700 Subject: [PATCH 0472/1178] Update for 6.7.1 release --- .coin-or/projDesc.xml | 4 ++-- README.md | 2 +- RELEASE.md | 5 +++-- pyomo/version/info.py | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index 1ee247e100f..da977677d1f 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.0 - 6.7.0 + 6.7.1 + 6.7.1 diff --git a/README.md b/README.md index 2f8a25403c2..95558e52a42 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ To get help from the Pyomo community ask a question on one of the following: ### Developers -Pyomo development moved to this repository in June, 2016 from +Pyomo development moved to this repository in June 2016 from Sandia National Laboratories. Developer discussions are hosted by [Google Groups](https://groups.google.com/forum/#!forum/pyomo-developers). diff --git a/RELEASE.md b/RELEASE.md index 8313c969f25..9b101e0999a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -We are pleased to announce the release of Pyomo 6.7.0. +We are pleased to announce the release of Pyomo 6.7.1. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing @@ -9,9 +9,10 @@ The following are highlights of the 6.7 release series: - Added support for Python 3.12 - Removed support for Python 3.7 - New writer for converting linear models to matrix form + - Improved handling of nested GDPs - New packages: - latex_printer (print Pyomo models to a LaTeX compatible format) - - contrib.solve: Part 1 of refactoring Pyomo's solver interfaces + - contrib.solver: preview of redesigned solver interfaces - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 0db00ac240f..dae1b6b6c7f 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -27,8 +27,8 @@ major = 6 minor = 7 micro = 1 -releaselevel = 'invalid' -# releaselevel = 'final' +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': From e7ec104640433f9e507dc7690664974075b4b9d9 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 10:39:18 -0700 Subject: [PATCH 0473/1178] Resetting main for development (6.7.2.dev0) --- pyomo/version/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index dae1b6b6c7f..de2efe83fb6 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 1 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 2 +releaselevel = 'invalid' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': From 04f68dcefedde1cab5b502778504e4189a647308 Mon Sep 17 00:00:00 2001 From: Shawn Martin Date: Wed, 21 Feb 2024 13:56:25 -0700 Subject: [PATCH 0474/1178] Integrated deprecated parmest UI into new UI. Removed deprecated folder. --- pyomo/contrib/parmest/deprecated/__init__.py | 10 - .../parmest/deprecated/examples/__init__.py | 10 - .../examples/reaction_kinetics/__init__.py | 10 - .../simple_reaction_parmest_example.py | 118 -- .../examples/reactor_design/__init__.py | 10 - .../reactor_design/bootstrap_example.py | 60 - .../reactor_design/datarec_example.py | 100 -- .../reactor_design/leaveNout_example.py | 98 -- .../likelihood_ratio_example.py | 64 - .../multisensor_data_example.py | 51 - .../parameter_estimation_example.py | 60 - .../examples/reactor_design/reactor_data.csv | 20 - .../reactor_data_multisensor.csv | 20 - .../reactor_data_timeseries.csv | 20 - .../examples/reactor_design/reactor_design.py | 104 -- .../reactor_design/timeseries_data_example.py | 56 - .../examples/rooney_biegler/__init__.py | 10 - .../rooney_biegler/bootstrap_example.py | 57 - .../likelihood_ratio_example.py | 62 - .../parameter_estimation_example.py | 60 - .../examples/rooney_biegler/rooney_biegler.py | 60 - .../rooney_biegler_with_constraint.py | 63 - .../deprecated/examples/semibatch/__init__.py | 10 - .../examples/semibatch/bootstrap_theta.csv | 101 -- .../deprecated/examples/semibatch/exp1.out | 1 - .../deprecated/examples/semibatch/exp10.out | 1 - .../deprecated/examples/semibatch/exp11.out | 1 - .../deprecated/examples/semibatch/exp12.out | 1 - .../deprecated/examples/semibatch/exp13.out | 1 - .../deprecated/examples/semibatch/exp14.out | 1 - .../deprecated/examples/semibatch/exp2.out | 1 - .../deprecated/examples/semibatch/exp3.out | 1 - .../deprecated/examples/semibatch/exp4.out | 1 - .../deprecated/examples/semibatch/exp5.out | 1 - .../deprecated/examples/semibatch/exp6.out | 1 - .../deprecated/examples/semibatch/exp7.out | 1 - .../deprecated/examples/semibatch/exp8.out | 1 - .../deprecated/examples/semibatch/exp9.out | 1 - .../examples/semibatch/obj_at_theta.csv | 1009 ------------ .../examples/semibatch/parallel_example.py | 57 - .../semibatch/parameter_estimation_example.py | 42 - .../examples/semibatch/scenario_example.py | 52 - .../examples/semibatch/scenarios.csv | 11 - .../examples/semibatch/semibatch.py | 287 ---- pyomo/contrib/parmest/deprecated/parmest.py | 1361 ----------------- .../parmest/deprecated/scenariocreator.py | 166 -- .../parmest/deprecated/tests/__init__.py | 10 - .../parmest/deprecated/tests/scenarios.csv | 11 - .../parmest/deprecated/tests/test_examples.py | 204 --- .../parmest/deprecated/tests/test_graphics.py | 68 - .../parmest/deprecated/tests/test_parmest.py | 956 ------------ .../deprecated/tests/test_scenariocreator.py | 146 -- .../parmest/deprecated/tests/test_solver.py | 75 - .../parmest/deprecated/tests/test_utils.py | 68 - pyomo/contrib/parmest/parmest.py | 1125 +++++++++++++- pyomo/contrib/parmest/scenariocreator.py | 74 +- pyomo/contrib/parmest/tests/test_parmest.py | 1048 ++++++++++++- .../parmest/tests/test_scenariocreator.py | 448 ++++++ 58 files changed, 2674 insertions(+), 5792 deletions(-) delete mode 100644 pyomo/contrib/parmest/deprecated/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py delete mode 100644 pyomo/contrib/parmest/deprecated/parmest.py delete mode 100644 pyomo/contrib/parmest/deprecated/scenariocreator.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/scenarios.csv delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_examples.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_graphics.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_parmest.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_solver.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_utils.py diff --git a/pyomo/contrib/parmest/deprecated/__init__.py b/pyomo/contrib/parmest/deprecated/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/__init__.py b/pyomo/contrib/parmest/deprecated/examples/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py deleted file mode 100644 index 719a930251c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ /dev/null @@ -1,118 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -''' -Example from Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) - -This example shows: -1. How to define the unknown (to be regressed parameters) with an index -2. How to call parmest to only estimate some of the parameters (and fix the rest) - -Code provided by Paul Akula. -''' - -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - RangeSet, - Expression, - minimize, - exp, - value, -) -import pyomo.contrib.parmest.parmest as parmest - - -def simple_reaction_model(data): - # Create the concrete model - model = ConcreteModel() - - model.x1 = Param(initialize=float(data['x1'])) - model.x2 = Param(initialize=float(data['x2'])) - - # Rate constants - model.rxn = RangeSet(2) - initial_guess = {1: 750, 2: 1200} - model.k = Var(model.rxn, initialize=initial_guess, within=PositiveReals) - - # reaction product - model.y = Expression(expr=exp(-model.k[1] * model.x1 * exp(-model.k[2] / model.x2))) - - # fix all of the regressed parameters - model.k.fix() - - # =================================================================== - # Stage-specific cost computations - def ComputeFirstStageCost_rule(model): - return 0 - - model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) - - def AllMeasurements(m): - return (float(data['y']) - m.y) ** 2 - - model.SecondStageCost = Expression(rule=AllMeasurements) - - def total_cost_rule(m): - return m.FirstStageCost + m.SecondStageCost - - model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) - - return model - - -def main(): - # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) - data = [ - {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, - {'experiment': 2, 'x1': 0.2, 'x2': 100, 'y': 0.983}, - {'experiment': 3, 'x1': 0.3, 'x2': 100, 'y': 0.955}, - {'experiment': 4, 'x1': 0.4, 'x2': 100, 'y': 0.979}, - {'experiment': 5, 'x1': 0.5, 'x2': 100, 'y': 0.993}, - {'experiment': 6, 'x1': 0.05, 'x2': 200, 'y': 0.626}, - {'experiment': 7, 'x1': 0.1, 'x2': 200, 'y': 0.544}, - {'experiment': 8, 'x1': 0.15, 'x2': 200, 'y': 0.455}, - {'experiment': 9, 'x1': 0.2, 'x2': 200, 'y': 0.225}, - {'experiment': 10, 'x1': 0.25, 'x2': 200, 'y': 0.167}, - {'experiment': 11, 'x1': 0.02, 'x2': 300, 'y': 0.566}, - {'experiment': 12, 'x1': 0.04, 'x2': 300, 'y': 0.317}, - {'experiment': 13, 'x1': 0.06, 'x2': 300, 'y': 0.034}, - {'experiment': 14, 'x1': 0.08, 'x2': 300, 'y': 0.016}, - {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, - ] - - # ======================================================================= - # Parameter estimation without covariance estimate - # Only estimate the parameter k[1]. The parameter k[2] will remain fixed - # at its initial value - theta_names = ['k[1]'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) - obj, theta = pest.theta_est() - print(obj) - print(theta) - print() - - # ======================================================================= - # Estimate both k1 and k2 and compute the covariance matrix - theta_names = ['k'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) - n = 15 # total number of data points used in the objective (y in 15 scenarios) - obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) - print(obj) - print(theta) - print(cov) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py deleted file mode 100644 index 3820b78c9b1..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Parameter estimation with bootstrap resampling - bootstrap_theta = pest.theta_est_bootstrap(50) - - # Plot results - parmest.graphics.pairwise_plot(bootstrap_theta, title="Bootstrap theta") - parmest.graphics.pairwise_plot( - bootstrap_theta, - theta, - 0.8, - ["MVN", "KDE", "Rect"], - title="Bootstrap theta with confidence regions", - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py deleted file mode 100644 index bae538f364c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py +++ /dev/null @@ -1,100 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - -np.random.seed(1234) - - -def reactor_design_model_for_datarec(data): - # Unfix inlet concentration for data rec - model = reactor_design_model(data) - model.caf.fixed = False - - return model - - -def generate_data(): - ### Generate data based on real sv, caf, ca, cb, cc, and cd - sv_real = 1.05 - caf_real = 10000 - ca_real = 3458.4 - cb_real = 1060.8 - cc_real = 1683.9 - cd_real = 1898.5 - - data = pd.DataFrame() - ndata = 200 - # Normal distribution, mean = 3400, std = 500 - data["ca"] = 500 * np.random.randn(ndata) + 3400 - # Random distribution between 500 and 1500 - data["cb"] = np.random.rand(ndata) * 1000 + 500 - # Lognormal distribution - data["cc"] = np.random.lognormal(np.log(1600), 0.25, ndata) - # Triangular distribution between 1000 and 2000 - data["cd"] = np.random.triangular(1000, 1800, 3000, size=ndata) - - data["sv"] = sv_real - data["caf"] = caf_real - - return data - - -def main(): - # Generate data - data = generate_data() - data_std = data.std() - - # Define sum of squared error objective function for data rec - def SSE(model, data): - expr = ( - ((float(data.iloc[0]["ca"]) - model.ca) / float(data_std["ca"])) ** 2 - + ((float(data.iloc[0]["cb"]) - model.cb) / float(data_std["cb"])) ** 2 - + ((float(data.iloc[0]["cc"]) - model.cc) / float(data_std["cc"])) ** 2 - + ((float(data.iloc[0]["cd"]) - model.cd) / float(data_std["cd"])) ** 2 - ) - return expr - - ### Data reconciliation - theta_names = [] # no variables to estimate, use initialized values - - pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) - - obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) - print(obj) - print(theta) - - parmest.graphics.grouped_boxplot( - data[["ca", "cb", "cc", "cd"]], - data_rec[["ca", "cb", "cc", "cd"]], - group_names=["Data", "Data Rec"], - ) - - ### Parameter estimation using reconciled data - theta_names = ["k1", "k2", "k3"] - data_rec["sv"] = data["sv"] - - pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) - obj, theta = pest.theta_est() - print(obj) - print(theta) - - theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} - print(theta_real) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py deleted file mode 100644 index d4ca9651753..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py +++ /dev/null @@ -1,98 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Create more data for the example - N = 50 - df_std = data.std().to_frame().transpose() - df_rand = pd.DataFrame(np.random.normal(size=N)) - df_sample = data.sample(N, replace=True).reset_index(drop=True) - data = df_sample + df_rand.dot(df_std) / 10 - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - print(obj) - print(theta) - - ### Parameter estimation with 'leave-N-out' - # Example use case: For each combination of data where one data point is left - # out, estimate theta - lNo_theta = pest.theta_est_leaveNout(1) - print(lNo_theta.head()) - - parmest.graphics.pairwise_plot(lNo_theta, theta) - - ### Leave one out/boostrap analysis - # Example use case: leave 25 data points out, run 20 bootstrap samples with the - # remaining points, determine if the theta estimate using the points left out - # is inside or outside an alpha region based on the bootstrap samples, repeat - # 5 times. Results are stored as a list of tuples, see API docs for information. - lNo = 25 - lNo_samples = 5 - bootstrap_samples = 20 - dist = "MVN" - alphas = [0.7, 0.8, 0.9] - - results = pest.leaveNout_bootstrap_test( - lNo, lNo_samples, bootstrap_samples, dist, alphas, seed=524 - ) - - # Plot results for a single value of alpha - alpha = 0.8 - for i in range(lNo_samples): - theta_est_N = results[i][1] - bootstrap_results = results[i][2] - parmest.graphics.pairwise_plot( - bootstrap_results, - theta_est_N, - alpha, - ["MVN"], - title="Alpha: " + str(alpha) + ", " + str(theta_est_N.loc[0, alpha]), - ) - - # Extract the percent of points that are within the alpha region - r = [results[i][1].loc[0, alpha] for i in range(lNo_samples)] - percent_true = sum(r) / len(r) - print(percent_true) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py deleted file mode 100644 index c47acf7f932..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py +++ /dev/null @@ -1,64 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -from itertools import product -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Find the objective value at each theta estimate - k1 = [0.8, 0.85, 0.9] - k2 = [1.6, 1.65, 1.7] - k3 = [0.00016, 0.000165, 0.00017] - theta_vals = pd.DataFrame(list(product(k1, k2, k3)), columns=["k1", "k2", "k3"]) - obj_at_theta = pest.objective_at_theta(theta_vals) - - # Run the likelihood ratio test - LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) - - # Plot results - parmest.graphics.pairwise_plot( - LR, theta, 0.9, title="LR results within 90% confidence region" - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py deleted file mode 100644 index 84c4abdf92a..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py +++ /dev/null @@ -1,51 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Parameter estimation using multisensor data - - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data, includes multiple sensors for ca and cc - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE_multisensor(model, data): - expr = ( - ((float(data.iloc[0]["ca1"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca2"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca3"]) - model.ca) ** 2) * (1 / 3) - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + ((float(data.iloc[0]["cc1"]) - model.cc) ** 2) * (1 / 2) - + ((float(data.iloc[0]["cc2"]) - model.cc) ** 2) * (1 / 2) - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) - obj, theta = pest.theta_est() - print(obj) - print(theta) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py deleted file mode 100644 index f5d9364097e..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - print(obj) - print(theta) - - # Assert statements compare parameter estimation (theta) to an expected value - k1_expected = 5.0 / 6.0 - k2_expected = 5.0 / 3.0 - k3_expected = 1.0 / 6000.0 - relative_error = abs(theta["k1"] - k1_expected) / k1_expected - assert relative_error < 0.05 - relative_error = abs(theta["k2"] - k2_expected) / k2_expected - assert relative_error < 0.05 - relative_error = abs(theta["k3"] - k3_expected) / k3_expected - assert relative_error < 0.05 - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv deleted file mode 100644 index c0695c049c4..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv +++ /dev/null @@ -1,20 +0,0 @@ -sv,caf,ca,cb,cc,cd -1.06,10010,3407.4,945.4,1717.1,1931.9 -1.11,10010,3631.6,1247.2,1694.1,1960.6 -1.16,10010,3645.3,971.4,1552.3,1898.8 -1.21,10002,3536.2,1225.9,1351.1,1757.0 -1.26,10002,3755.6,1263.8,1562.3,1952.2 -1.30,10007,3598.3,1153.4,1413.4,1903.3 -1.35,10007,3939.0,971.4,1416.9,1794.9 -1.41,10009,4227.9,986.3,1188.7,1821.5 -1.45,10001,4163.1,972.5,1085.6,1908.7 -1.50,10002,3896.3,977.3,1132.9,2080.5 -1.56,10004,3801.6,1040.6,1157.7,1780.0 -1.60,10008,4128.4,1198.6,1150.0,1581.9 -1.66,10002,4385.4,1158.7,970.0,1629.8 -1.70,10007,3960.8,1194.9,1091.2,1835.5 -1.76,10007,4180.8,1244.2,1034.8,1739.5 -1.80,10001,4212.3,1240.7,1010.3,1739.6 -1.85,10004,4200.2,1164.0,931.5,1783.7 -1.90,10009,4748.6,1037.9,1065.9,1685.6 -1.96,10009,4941.3,1038.5,996.0,1855.7 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv deleted file mode 100644 index 9df745a8422..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv +++ /dev/null @@ -1,20 +0,0 @@ -sv,caf,ca1,ca2,ca3,cb,cc1,cc2,cd -1.06,10010,3407.4,3363.1,3759.1,945.4,1717.1,1695.1,1931.9 -1.11,10010,3631.6,3345.2,3906.0,1247.2,1694.1,1536.7,1960.6 -1.16,10010,3645.3,3784.9,3301.3,971.4,1552.3,1496.2,1898.8 -1.21,10002,3536.2,3718.3,3678.5,1225.9,1351.1,1549.7,1757.0 -1.26,10002,3755.6,3731.8,3854.7,1263.8,1562.3,1410.1,1952.2 -1.30,10007,3598.3,3751.6,3722.5,1153.4,1413.4,1291.6,1903.3 -1.35,10007,3939.0,3969.5,3827.2,971.4,1416.9,1276.8,1794.9 -1.41,10009,4227.9,3721.3,4046.7,986.3,1188.7,1221.0,1821.5 -1.45,10001,4163.1,4142.7,4512.1,972.5,1085.6,1212.1,1908.7 -1.50,10002,3896.3,3953.7,4028.0,977.3,1132.9,1167.7,2080.5 -1.56,10004,3801.6,4263.3,4015.3,1040.6,1157.7,1236.5,1780.0 -1.60,10008,4128.4,4061.1,3914.8,1198.6,1150.0,1032.2,1581.9 -1.66,10002,4385.4,4344.7,4006.8,1158.7,970.0,1155.1,1629.8 -1.70,10007,3960.8,4259.1,4274.7,1194.9,1091.2,958.6,1835.5 -1.76,10007,4180.8,4071.1,4598.7,1244.2,1034.8,1086.8,1739.5 -1.80,10001,4212.3,4541.8,4440.0,1240.7,1010.3,920.8,1739.6 -1.85,10004,4200.2,4444.9,4667.2,1164.0,931.5,850.7,1783.7 -1.90,10009,4748.6,4813.4,4753.2,1037.9,1065.9,898.5,1685.6 -1.96,10009,4941.3,4511.8,4405.4,1038.5,996.0,921.9,1855.7 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv deleted file mode 100644 index 1421cfef6a0..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv +++ /dev/null @@ -1,20 +0,0 @@ -experiment,time,sv,caf,ca,cb,cc,cd -0,18000,1.075,10008,3537.5,1077.2,1591.2,1938.7 -0,18060,1.121,10002,3547.7,1186.2,1766.3,1946.9 -0,18120,1.095,10005,3614.4,1009.9,1702.9,1841.8 -0,18180,1.102,10007,3443.7,863.1,1666.2,1918.7 -0,18240,1.105,10002,3687.1,1052.1,1501.7,1905.0 -0,18300,1.084,10008,3452.7,1000.5,1512.0,2043.4 -1,18360,1.159,10009,3427.8,1133.1,1481.1,1837.1 -1,18420,1.432,10010,4029.8,1058.8,1213.0,1911.1 -1,18480,1.413,10005,3953.1,960.1,1304.8,1754.3 -1,18540,1.475,10008,4034.8,1121.2,1351.0,1992.0 -1,18600,1.433,10002,4029.8,1100.6,1199.5,1713.9 -1,18660,1.488,10006,3972.8,1148.0,1380.7,1992.1 -1,18720,1.456,10003,4031.2,1145.2,1133.1,1812.6 -2,18780,1.821,10008,4499.1,980.8,924.7,1840.9 -2,18840,1.856,10005,4370.9,1000.7,833.4,1848.4 -2,18900,1.846,10002,4438.6,1038.6,1042.8,1703.3 -2,18960,1.852,10002,4468.4,1151.8,1119.1,1564.8 -2,19020,1.865,10009,4341.6,1060.5,844.2,1974.8 -2,19080,1.872,10002,4427.0,964.6,840.2,1928.5 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py deleted file mode 100644 index 16f65e236eb..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py +++ /dev/null @@ -1,104 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -""" -Continuously stirred tank reactor model, based on -pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py -""" -import pandas as pd -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - maximize, - SolverFactory, -) - - -def reactor_design_model(data): - # Create the concrete model - model = ConcreteModel() - - # Rate constants - model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 - model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 - model.k3 = Param( - initialize=1.0 / 6000.0, within=PositiveReals, mutable=True - ) # m^3/(gmol min) - - # Inlet concentration of A, gmol/m^3 - if isinstance(data, dict) or isinstance(data, pd.Series): - model.caf = Param(initialize=float(data["caf"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.caf = Param(initialize=float(data.iloc[0]["caf"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") - - # Space velocity (flowrate/volume) - if isinstance(data, dict) or isinstance(data, pd.Series): - model.sv = Param(initialize=float(data["sv"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.sv = Param(initialize=float(data.iloc[0]["sv"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") - - # Outlet concentration of each component - model.ca = Var(initialize=5000.0, within=PositiveReals) - model.cb = Var(initialize=2000.0, within=PositiveReals) - model.cc = Var(initialize=2000.0, within=PositiveReals) - model.cd = Var(initialize=1000.0, within=PositiveReals) - - # Objective - model.obj = Objective(expr=model.cb, sense=maximize) - - # Constraints - model.ca_bal = Constraint( - expr=( - 0 - == model.sv * model.caf - - model.sv * model.ca - - model.k1 * model.ca - - 2.0 * model.k3 * model.ca**2.0 - ) - ) - - model.cb_bal = Constraint( - expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) - ) - - model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) - - model.cd_bal = Constraint( - expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) - ) - - return model - - -def main(): - # For a range of sv values, return ca, cb, cc, and cd - results = [] - sv_values = [1.0 + v * 0.05 for v in range(1, 20)] - caf = 10000 - for sv in sv_values: - model = reactor_design_model(pd.DataFrame(data={"caf": [caf], "sv": [sv]})) - solver = SolverFactory("ipopt") - solver.solve(model) - results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) - - results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) - print(results) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py deleted file mode 100644 index e7acefc2224..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py +++ /dev/null @@ -1,56 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname - -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) -from pyomo.contrib.parmest.deprecated.parmest import group_data - - -def main(): - # Parameter estimation using timeseries data - - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - - # Data, includes multiple sensors for ca and cc - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) - data = pd.read_csv(file_name) - - # Group time series data into experiments, return the mean value for sv and caf - # Returns a list of dictionaries - data_ts = group_data(data, 'experiment', ['sv', 'caf']) - - def SSE_timeseries(model, data): - expr = 0 - for val in data['ca']: - expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) - for val in data['cb']: - expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) - for val in data['cc']: - expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) - for val in data['cd']: - expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) - return expr - - pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) - obj, theta = pest.theta_est() - print(obj) - print(theta) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py deleted file mode 100644 index f686bbd933d..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py +++ /dev/null @@ -1,57 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, -) - - -def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] - - # Data - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Parameter estimation with bootstrap resampling - bootstrap_theta = pest.theta_est_bootstrap(50, seed=4581) - - # Plot results - parmest.graphics.pairwise_plot(bootstrap_theta, title='Bootstrap theta') - parmest.graphics.pairwise_plot( - bootstrap_theta, - theta, - 0.8, - ['MVN', 'KDE', 'Rect'], - title='Bootstrap theta with confidence regions', - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py deleted file mode 100644 index 5e54a33abda..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py +++ /dev/null @@ -1,62 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -from itertools import product -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, -) - - -def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] - - # Data - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Find the objective value at each theta estimate - asym = np.arange(10, 30, 2) - rate = np.arange(0, 1.5, 0.1) - theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=['asymptote', 'rate_constant'] - ) - obj_at_theta = pest.objective_at_theta(theta_vals) - - # Run the likelihood ratio test - LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) - - # Plot results - parmest.graphics.pairwise_plot( - LR, theta, 0.8, title='LR results within 80% confidence region' - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py deleted file mode 100644 index 9af33217fe4..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, -) - - -def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] - - # Data - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) - - # Parameter estimation and covariance - n = 6 # total number of data points used in the objective (y in 6 scenarios) - obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) - - # Plot theta estimates using a multivariate Gaussian distribution - parmest.graphics.pairwise_plot( - (theta, cov, 100), - theta_star=theta, - alpha=0.8, - distributions=['MVN'], - title='Theta estimates within 80% confidence region', - ) - - # Assert statements compare parameter estimation (theta) to an expected value - relative_error = abs(theta['asymptote'] - 19.1426) / 19.1426 - assert relative_error < 0.01 - relative_error = abs(theta['rate_constant'] - 0.5311) / 0.5311 - assert relative_error < 0.01 - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py deleted file mode 100644 index 5a0e1238e85..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for -model parameter uncertainty using nonlinear confidence regions. AIChE Journal, -47(8), 1794-1804. -""" - -import pandas as pd -import pyomo.environ as pyo - - -def rooney_biegler_model(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Var(initialize=15) - model.rate_constant = pyo.Var(initialize=0.5) - - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - def SSE_rule(m): - return sum( - (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index - ) - - model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) - - return model - - -def main(): - # These were taken from Table A1.4 in Bates and Watts (1988). - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - model = rooney_biegler_model(data) - solver = pyo.SolverFactory('ipopt') - solver.solve(model) - - print('asymptote = ', model.asymptote()) - print('rate constant = ', model.rate_constant()) - - -if __name__ == '__main__': - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py deleted file mode 100644 index 2582e3fe928..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ /dev/null @@ -1,63 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for -model parameter uncertainty using nonlinear confidence regions. AIChE Journal, -47(8), 1794-1804. -""" - -import pandas as pd -import pyomo.environ as pyo - - -def rooney_biegler_model_with_constraint(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Var(initialize=15) - model.rate_constant = pyo.Var(initialize=0.5) - model.response_function = pyo.Var(data.hour, initialize=0.0) - - # changed from expression to constraint - def response_rule(m, h): - return m.response_function[h] == m.asymptote * ( - 1 - pyo.exp(-m.rate_constant * h) - ) - - model.response_function_constraint = pyo.Constraint(data.hour, rule=response_rule) - - def SSE_rule(m): - return sum( - (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index - ) - - model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) - - return model - - -def main(): - # These were taken from Table A1.4 in Bates and Watts (1988). - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - model = rooney_biegler_model_with_constraint(data) - solver = pyo.SolverFactory('ipopt') - solver.solve(model) - - print('asymptote = ', model.asymptote()) - print('rate constant = ', model.rate_constant()) - - -if __name__ == '__main__': - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv deleted file mode 100644 index 29923a782c5..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv +++ /dev/null @@ -1,101 +0,0 @@ -,k1,k2,E1,E2 -0,23.8359813557911,149.99999125263844,31164.260824269295,41489.69422529956 -1,19.251987486659512,105.3374117880675,30505.86059307485,40516.897897740404 -2,19.31940450911214,105.78105886426505,30509.636888745794,40539.53548872927 -3,8.754357429283429,149.99988037658665,28334.500331107014,41482.01554893696 -4,23.016722464092286,80.03743984792878,31091.61503716734,39770.08415278276 -5,6.612337410520649,140.0259411600077,27521.46259880474,41302.159495413296 -6,14.29348509961158,147.0817016641302,29605.749859593245,41443.12009807534 -7,14.152069480386153,149.9914759675382,29676.633227079245,41483.41029455195 -8,19.081046896092914,125.55106106390114,30586.57857977985,41005.60243351924 -9,3.063566173952205,149.9999684548014,25473.079370305273,41483.426370389796 -10,17.79494440791066,108.52425726918327,30316.710830136202,40618.63715404914 -11,97.307579412204,149.99998972597675,35084.37589956093,41485.835559276136 -12,20.793042577945116,91.124365144131,30782.17494940993,40138.215713547994 -13,12.740540794730641,89.86327635412908,29396.65520336387,40086.14665722912 -14,6.930810780299319,149.99999327266906,27667.0240033497,41480.188987754496 -15,20.29404799567638,101.07539817765885,30697.087258737916,40443.316578889426 -16,85.77501788788223,149.99996482096984,34755.77375009206,41499.23448818336 -17,24.13150325243255,77.06876294766496,31222.03914354306,39658.418332258894 -18,16.026645517712712,149.99993094015056,30015.46332620076,41490.69892111652 -19,31.020018442708537,62.11558789585982,31971.311996398897,39089.828285017575 -20,20.815008037484656,87.35968459422139,30788.643843293,40007.78137819648 -21,19.007148519616447,96.44320176694993,30516.36933261116,40284.23312198372 -22,22.232021812057308,89.71692873746096,30956.252845068626,40095.009765519 -23,16.830765834427297,120.65209863104229,30139.92208332896,40912.673450399234 -24,15.274799190396566,129.82767733073857,29780.055282261117,41078.04749417758 -25,22.37343657709118,82.32861355430458,31013.57952062852,39853.06284595207 -26,9.055694749134819,149.99987339406314,28422.482259116612,41504.97564187301 -27,19.909770949417275,86.5634026379812,30705.60369894775,39996.134938503914 -28,20.604557306290886,87.96473948102359,30786.467003867263,40051.28176004557 -29,21.94101237923462,88.18216423767153,30942.372558158557,40051.20357069738 -30,3.200663718121338,149.99997712051055,25472.46099917771,41450.884180452646 -31,20.5812467558026,86.36098672832426,30802.74421085271,40010.76777825347 -32,18.776139793586893,108.99943042186453,30432.474809193136,40641.48011315501 -33,17.14246930769276,112.29370332257908,30164.332101438307,40684.867629869856 -34,20.52146255576043,99.7078140453859,30727.90573864389,40401.20730725967 -35,17.05073306185531,66.00385439687035,30257.075479935145,39247.26647870223 -36,7.1238843213074015,51.05163218895348,27811.250260416655,38521.11199236329 -37,10.54291332571747,76.74902426944477,28763.52244085013,39639.92644514267 -38,16.329028964122656,107.60037882134996,30073.5111433796,40592.825374177235 -39,18.0923131790489,107.75659679748213,30355.62290415686,40593.10521263782 -40,15.477264179087811,149.99995828085014,29948.62617372307,41490.770726165414 -41,23.190670255199933,76.5654091811839,31107.96477489951,39635.650879492074 -42,20.34720227734719,90.07051780196629,30716.131795936217,40096.932765428995 -43,23.60627359054596,80.0847207027996,31130.449736501876,39756.06693747353 -44,22.54968153535252,83.72995448206636,31038.51932262643,39906.60181934743 -45,24.951320839961582,67.97010976959977,31356.00147390564,39307.75709154711 -46,61.216667588824386,149.9999967830529,33730.22100500659,41474.80665231048 -47,9.797300324197744,136.33054557076974,28588.83540859912,41222.22413163186 -48,21.75078861615545,139.82641444329093,30894.847060525986,41290.16131583715 -49,21.76324066920255,99.57885291658233,30860.292260186063,40386.00605205238 -50,20.244262248110417,86.2553098058883,30742.054735645124,39981.83946305757 -51,21.859217291379004,72.89837327878459,30999.703939831277,39514.23768439393 -52,20.902111153308944,88.36862895882298,30782.76240691508,40033.44884393017 -53,59.58504995089654,149.9999677447201,33771.647879014425,41496.69202917452 -54,21.63994234351529,80.9641923004028,30933.578583737795,39809.523930207484 -55,9.804873383156298,149.9995892138235,28729.93818644509,41500.94496844104 -56,9.517359502437172,149.99308840029815,28505.329315103318,41470.65218792529 -57,19.923610217578116,88.23847592895486,30636.024864041487,40020.79650218989 -58,20.366495228182394,85.1991151089578,30752.560133063143,39947.719888972904 -59,12.242715793208157,149.99998097746882,29308.42752633667,41512.25071862387 -60,19.677765799324447,97.30674967097808,30618.37668428642,40323.0499230797 -61,19.03651315222424,109.20775378637025,30455.39615515442,40614.722801684395 -62,21.37660531151217,149.99999616215425,30806.121697474813,41479.3976433347 -63,21.896838392882998,86.86206456282005,30918.823491874144,39986.262281131254 -64,5.030122322262226,149.99991736085678,26792.302062236955,41480.579525893794 -65,17.851755694421776,53.33521102556455,30419.017295420916,38644.47349861614 -66,20.963796542255896,90.72302887846234,30795.751244616677,40114.19163802526 -67,23.082992539267945,77.24345020180209,31107.07485019312,39665.22410226011 -68,18.953050386839383,90.80802949182345,30529.280393040182,40113.73467038244 -69,20.710937910951355,83.16996057131982,30805.892332796295,39876.270184728084 -70,18.18549080794899,65.72657652078952,30416.294615296756,39223.21339606898 -71,12.147892028456324,45.12945045196771,29302.888575028635,38194.144730342545 -72,4.929663537166405,133.89086200105797,26635.8524254091,41163.82082194103 -73,20.512731504598662,106.98199797354127,30660.67479570742,40560.70063653076 -74,21.006700520199008,93.35471748418676,30761.272887418058,40178.10564855804 -75,19.73635577733317,98.75362910260881,30599.64039254174,40346.31274388047 -76,3.6393630101175565,149.99998305638113,25806.925407145678,41446.42489819377 -77,14.430958212981363,149.9999928114441,29710.277666486683,41478.96029884101 -78,21.138173237661093,90.73414659450283,30833.36092609432,40128.61898313504 -79,19.294823672883208,104.69324605284973,30510.371654343133,40510.84889949937 -80,2.607050470695225,69.22680095813037,25000.001468502505,39333.142090801295 -81,16.949842823156228,118.76691429120146,30074.04126731665,40824.66852388976 -82,21.029588811317897,95.27115352081795,30770.753828753943,40243.47156167542 -83,18.862418349044077,111.08370690591005,30421.17882623639,40670.941374189555 -84,24.708015660945147,76.24225941680999,31286.7038829574,39632.545034540664 -85,21.58937721477476,92.6329553952883,30871.989108388123,40181.7478528116 -86,21.091322126816706,96.07721666941696,30765.91144819689,40265.321194575095 -87,19.337815749868728,96.50567420686403,30604.551156564357,40318.12321325275 -88,17.77732130279279,108.5062535737451,30287.456682982094,40602.76307166587 -89,15.259532609396405,134.79914728383426,29793.69015375863,41199.11159557717 -90,21.616910309091583,90.65235108674251,30848.137718134392,40096.0776408459 -91,3.3372937891220475,149.99991062247588,25630.388452101062,41483.30064805118 -92,20.652437906744403,97.86062128528714,30747.864718937744,40330.11871286893 -93,22.134113060054425,73.68464943802763,31013.225174702933,39535.65213713519 -94,20.297310066178802,93.79207093658654,30684.309981457223,40191.747572763874 -95,6.007958386675472,149.99997175883215,27126.707007542,41465.75099589974 -96,16.572749402536758,40.75746000309888,30154.396795028595,37923.85448825053 -97,21.235697111801056,98.97798760165126,30807.097617165928,40373.550932032136 -98,20.10350615639414,96.19608053749371,30632.029399836003,40258.3813340696 -99,18.274272179970747,96.49060573948069,30456.872524151822,40305.258325587834 diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out deleted file mode 100644 index f1d826085bf..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 1, "Ca0": 0, "Ca_meas": {"17280.0": 1.0206137429621787, "0.0": 0.3179205033819153, "7560.0": 6.1190611079452015, "9720.0": 5.143125125521654, "19440.0": 0.6280402056097951, "16200.0": 0.8579867036528984, "11880.0": 3.7282923165210042, "2160.0": 4.63887641485678, "8640.0": 5.343033989137008, "14040.0": 2.053029378587664, "4320.0": 6.161603379127277, "6480.0": 6.175522427215327, "1080.0": 2.5735849991352358, "18360.0": 0.6351040530590654, "10800.0": 5.068206847862049, "21600.0": 0.40727295182614354, "20520.0": 0.6621175064161002, "5400.0": 6.567824349703669, "3240.0": 5.655458079751501, "12960.0": 2.654764659666162, "15120.0": 1.6757350275784135}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 5.987636915771096, "0.0": 0.09558280565339891, "7560.0": 1.238130321602845, "9720.0": 1.9945247030805529, "19440.0": 6.695864385679773, "16200.0": 5.41645220805244, "11880.0": 2.719892366798277, "2160.0": 0.10805070272409367, "8640.0": 1.800229763433655, "14040.0": 4.156268601598023, "4320.0": -0.044818714779864405, "6480.0": 0.8106022415380871, "1080.0": -0.07327388848369068, "18360.0": 5.96868114596425, "10800.0": 2.0726982059573835, "21600.0": 7.269213818513372, "20520.0": 6.725777234409265, "5400.0": 0.18749831830326769, "3240.0": -0.10164819461093579, "12960.0": 3.745361461163259, "15120.0": 4.92464438752146}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.527811727243744, "0.0": 0.006198551839384427, "7560.0": 8.198459268980448, "9720.0": 10.884163155983586, "19440.0": 12.150552321810109, "16200.0": 13.247640677577017, "11880.0": 12.921639059281906, "2160.0": 1.2393091651113075, "8640.0": 9.76716833273541, "14040.0": 13.211149989298647, "4320.0": 3.803104433804622, "6480.0": 6.5810650565269375, "1080.0": 0.3042714459761661, "18360.0": 12.544400522361945, "10800.0": 11.737104197836604, "21600.0": 11.886358606219954, "20520.0": 11.832544691029744, "5400.0": 5.213810980890077, "3240.0": 2.1926632109587216, "12960.0": 13.113839789286594, "15120.0": 13.27982750652159}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 325.16059207223026, "0.0": 297.66636064503314, "7560.0": 331.3122493868531, "9720.0": 332.5912691607457, "19440.0": 325.80525903012233, "16200.0": 323.2692288871708, "11880.0": 334.0549081870754, "2160.0": 323.2373236557714, "8640.0": 333.4576764024497, "14040.0": 328.42212335544315, "4320.0": 327.6704558317418, "6480.0": 331.06042780025075, "1080.0": 316.2567029216892, "18360.0": 326.6647586865489, "10800.0": 334.23136746878185, "21600.0": 324.0057232633869, "20520.0": 324.4555288383823, "5400.0": 331.9568676813546, "3240.0": 326.12583828081813, "12960.0": 329.2382904744002, "15120.0": 327.3959354386782}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out deleted file mode 100644 index 7eb7980a7be..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 10, "Ca0": 0, "Ca_meas": {"0.0": 0.17585381115505311, "4320.0": 6.0315379232850992, "7560.0": 5.4904391073929908, "21600.0": 0.28272237229599306, "9720.0": 5.2677178238115365, "16200.0": 1.1265897978788217, "20520.0": 0.057584376823091032, "3240.0": 5.6315955838815661, "11880.0": 3.328489578835276, "14040.0": 2.0226562017072607, "17280.0": 1.1268539727440208, "2160.0": 4.3030132489621638, "5400.0": 6.1094034780709618, "18360.0": 0.50886621390394615, "8640.0": 5.3773941013828752, "6480.0": 5.9760510402178078, "1080.0": 2.9280667525762598, "15120.0": 1.375026987701359, "12960.0": 2.5451999496997635, "19440.0": 0.79349685535634917, "10800.0": 4.3653401523141229}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.038024312530073517, "4320.0": 0.35646997778490641, "7560.0": 1.0986283962244756, "21600.0": 7.160087143091391, "9720.0": 2.129148884681515, "16200.0": 5.1383561968992435, "20520.0": 6.8451793517536901, "3240.0": 0.098714783055484312, "11880.0": 3.0269500169512602, "14040.0": 3.9370558676788283, "17280.0": 5.9641262824404357, "2160.0": -0.12281730158248855, "5400.0": 0.59307341448224149, "18360.0": 6.2121451052794248, "8640.0": 1.7607685730069123, "6480.0": 0.53516134735284115, "1080.0": 0.021830284057365701, "15120.0": 4.7082270119144871, "12960.0": 4.0629501433813449, "19440.0": 6.333023537154518, "10800.0": 2.2805891192921983}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.063620932422370907, "4320.0": 3.5433262677921662, "7560.0": 8.6893371808300444, "21600.0": 11.870505989648386, "9720.0": 11.309193344250055, "16200.0": 12.739396897287321, "20520.0": 11.791007739959538, "3240.0": 2.1799284210951009, "11880.0": 12.669418985545658, "14040.0": 13.141014574935076, "17280.0": 12.645153211711902, "2160.0": 1.4830589148905116, "5400.0": 5.2739635750232985, "18360.0": 12.761210138866151, "8640.0": 9.9142303856203373, "6480.0": 7.0761548290524603, "1080.0": 0.43050918133895111, "15120.0": 12.66028651303237, "12960.0": 12.766057551719733, "19440.0": 12.385465894826957, "10800.0": 11.96758252080965}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.47604965958681, "4320.0": 327.40308974239883, "7560.0": 332.84954650085029, "21600.0": 322.5157157706418, "9720.0": 333.3451281132692, "16200.0": 325.64418049630734, "20520.0": 321.94339225425767, "3240.0": 327.19623764314332, "11880.0": 330.24784399520615, "14040.0": 328.20666366981681, "17280.0": 324.73232197103431, "2160.0": 324.1280979971192, "5400.0": 330.87716394833132, "18360.0": 323.47482388751507, "8640.0": 332.49299626233665, "6480.0": 332.1275559564059, "1080.0": 319.29329353123859, "15120.0": 327.29837672678497, "12960.0": 329.24537484541742, "19440.0": 324.02559861322572, "10800.0": 335.57159429203386}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out deleted file mode 100644 index d97442f031b..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 11, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9214639882977118, "7560.0": 3.782748575728669, "21600.0": 1.0934559688831289, "9720.0": 3.9988602724163731, "16200.0": 1.3975369118671503, "20520.0": 1.1243254110346068, "3240.0": 2.4246081576650433, "11880.0": 3.4755989173839743, "14040.0": 1.9594736461287787, "17280.0": 1.2827751626299488, "2160.0": 1.7884659182526941, "5400.0": 3.3009088212806073, "18360.0": 1.2111862780556402, "8640.0": 3.9172974313558302, "6480.0": 3.5822886585117493, "1080.0": 0.9878678077907459, "15120.0": 1.5972116122332332, "12960.0": 2.5920858659103394, "19440.0": 1.1618336860102094, "10800.0": 4.0378190270354528}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.023982318123109209, "7560.0": 0.12357850466238102, "21600.0": 6.5347744286121001, "9720.0": 0.24908724520575926, "16200.0": 3.2484340438243127, "20520.0": 5.9039946620569568, "3240.0": 0.0099959742916886744, "11880.0": 0.58037814751790051, "14040.0": 1.8149241834100336, "17280.0": 3.9360275733370447, "2160.0": 0.0028162554877217529, "5400.0": 0.046614709277751604, "18360.0": 4.6059327483829717, "8640.0": 0.17995429953061945, "6480.0": 0.079417046516631534, "1080.0": 0.00029995464126276485, "15120.0": 2.5396785773426069, "12960.0": 1.1193190878564474, "19440.0": 5.2613272888405183, "10800.0": 0.33137635542084787}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.96588936445839646, "7560.0": 2.5063363714379272, "21600.0": 7.0191274767686718, "9720.0": 3.6738114082888234, "16200.0": 7.2205659498627206, "20520.0": 7.0929480434376666, "3240.0": 0.56824903663740756, "11880.0": 5.2689545424581627, "14040.0": 6.8616827734843717, "17280.0": 7.2356915893004699, "2160.0": 0.25984959276122965, "5400.0": 1.4328144788513235, "18360.0": 7.2085405027857679, "8640.0": 3.0841896398822519, "6480.0": 1.9514434452676097, "1080.0": 0.063681239951285565, "15120.0": 7.1238797146821753, "12960.0": 6.279843675978368, "19440.0": 7.1578041487575703, "10800.0": 4.2664529460540459}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.23882460110946, "7560.0": 313.65588947948061, "21600.0": 347.47264008527429, "9720.0": 314.23459379740643, "16200.0": 349.53974420970837, "20520.0": 347.6739307915775, "3240.0": 311.5507050941913, "11880.0": 343.84565806826117, "14040.0": 351.77400891772504, "17280.0": 348.76203275606656, "2160.0": 310.56857709679934, "5400.0": 312.79850843986321, "18360.0": 348.2596349485566, "8640.0": 313.9757607933924, "6480.0": 313.26649244154709, "1080.0": 308.41052123547064, "15120.0": 350.65940715239384, "12960.0": 351.11078111562267, "19440.0": 347.92214750818005, "10800.0": 317.60632266285268}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out deleted file mode 100644 index cbed2d89634..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 12, "Ca0": 0, "Ca_meas": {"0.0": -0.11670041274142862, "4320.0": 2.5843604031503551, "7560.0": 3.9909358380362421, "21600.0": 0.85803342268297711, "9720.0": 3.8011636431252702, "16200.0": 1.1002575863333801, "20520.0": 1.5248654901477563, "3240.0": 2.8626873137030655, "11880.0": 3.0876088469734766, "14040.0": 2.1233520630854348, "17280.0": 1.3118009790549121, "2160.0": 1.9396713478350336, "5400.0": 3.4898817799323192, "18360.0": 1.3214498335778544, "8640.0": 4.3166520687122008, "6480.0": 3.7430014080130212, "1080.0": 0.71502131244112932, "15120.0": 1.6869248126824714, "12960.0": 2.6199132141077999, "19440.0": 1.1934026369102775, "10800.0": 4.1662760005238244}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.14625373198142194, "4320.0": 0.50521240190855021, "7560.0": 0.072658369151157948, "21600.0": 6.5820277701997947, "9720.0": 0.14656574869775074, "16200.0": 3.2344935292984842, "20520.0": 6.1934992398225113, "3240.0": 0.073187422926812754, "11880.0": 0.68271223751792398, "14040.0": 1.5495094265280573, "17280.0": 3.5758298262936474, "2160.0": -0.052665808995189155, "5400.0": -0.067101579134353218, "18360.0": 4.6809081069906799, "8640.0": 0.58099071333349073, "6480.0": -0.34905530826754594, "1080.0": -0.13981627097780677, "15120.0": 2.7577781806493582, "12960.0": 0.9824219783559841, "19440.0": 5.2002977724609245, "10800.0": 0.0089889440138224419}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.34864609349591541, "4320.0": 0.99561164186682227, "7560.0": 2.4688230226633143, "21600.0": 7.0564292657160461, "9720.0": 3.9961025255558669, "16200.0": 7.161739218807984, "20520.0": 6.8044634236188921, "3240.0": 0.21017912447011355, "11880.0": 5.1168427571991435, "14040.0": 7.0032016822280907, "17280.0": 7.11845364876228, "2160.0": 0.22241873726625405, "5400.0": 1.3174801426799267, "18360.0": 6.9581816529657257, "8640.0": 3.0438444011178785, "6480.0": 2.2558063612162291, "1080.0": 0.38969247867806489, "15120.0": 7.2125210995495488, "12960.0": 6.4014182164755429, "19440.0": 6.8128450220608308, "10800.0": 4.2649851849420299}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.53274252012955, "4320.0": 311.15339067777529, "7560.0": 312.18867239090878, "21600.0": 346.00646319366473, "9720.0": 313.61710665630221, "16200.0": 352.81825567952984, "20520.0": 346.66248325950249, "3240.0": 311.37928873928001, "11880.0": 343.17457873193757, "14040.0": 352.88609842940627, "17280.0": 348.48519596719899, "2160.0": 312.70676562686674, "5400.0": 314.29841143358993, "18360.0": 347.81967845794014, "8640.0": 313.26528616120316, "6480.0": 314.33539425367189, "1080.0": 308.35955588183077, "15120.0": 350.62179387183005, "12960.0": 348.72513776404708, "19440.0": 346.65341312375318, "10800.0": 318.79995964600641}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out deleted file mode 100644 index 6ef514c951a..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 13, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9015294551493156, "7560.0": 3.7456240714596114, "21600.0": 1.0893473942847287, "9720.0": 3.955661759695563, "16200.0": 1.3896514099314119, "20520.0": 1.1200835239496501, "3240.0": 2.4116751003091821, "11880.0": 3.4212519616089123, "14040.0": 1.9331934810250495, "17280.0": 1.2772682671606199, "2160.0": 1.7821342925311514, "5400.0": 3.2743367747379883, "18360.0": 1.2065189956942144, "8640.0": 3.8765800278544793, "6480.0": 3.5499054339456388, "1080.0": 0.98643229677676525, "15120.0": 1.583391359829623, "12960.0": 2.5471212725882397, "19440.0": 1.1574522594063252, "10800.0": 3.9931029485836738}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.025294213033754839, "7560.0": 0.12897041691694733, "21600.0": 6.5640983670402253, "9720.0": 0.2583989163781088, "16200.0": 3.2753828012649455, "20520.0": 5.9325635171128175, "3240.0": 0.01057908838025531, "11880.0": 0.60077017176350533, "14040.0": 1.8465767045073862, "17280.0": 3.9624456898675042, "2160.0": 0.0029862658207987017, "5400.0": 0.048985517519306479, "18360.0": 4.6327886545170172, "8640.0": 0.1872203610374778, "6480.0": 0.083160507067847278, "1080.0": 0.0003160950335645278, "15120.0": 2.5686484578016535, "12960.0": 1.149697840345306, "19440.0": 5.2890172904133799, "10800.0": 0.3428648031290083}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.98451200269614558, "7560.0": 2.5380689634524152, "21600.0": 6.993912112938939, "9720.0": 3.7076982498372795, "16200.0": 7.2015026943578233, "20520.0": 7.0686210754667576, "3240.0": 0.58059897990470211, "11880.0": 5.3029094739876195, "14040.0": 6.8563104174907483, "17280.0": 7.2147803682393352, "2160.0": 0.26601120814969509, "5400.0": 1.4570157171523839, "18360.0": 7.1863518790131433, "8640.0": 3.1176409818767401, "6480.0": 1.9800832092825005, "1080.0": 0.06510061057296454, "15120.0": 7.1087300866267373, "12960.0": 6.294429516811606, "19440.0": 7.1344955737885876, "10800.0": 4.2996805767976616}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.83405362164399, "7560.0": 314.10857941130064, "21600.0": 347.58975942376372, "9720.0": 314.61233912755387, "16200.0": 349.56203914424219, "20520.0": 347.79317623855678, "3240.0": 312.2016171592112, "11880.0": 344.43453363173575, "14040.0": 351.72789167129031, "17280.0": 348.83759825667926, "2160.0": 311.27622091507072, "5400.0": 313.34232236658977, "18360.0": 348.36453268948424, "8640.0": 314.38892058203726, "6480.0": 313.76278431242508, "1080.0": 309.11491437259622, "15120.0": 350.61679816631096, "12960.0": 351.27269989378158, "19440.0": 348.03901542922108, "10800.0": 318.0555419842903}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out deleted file mode 100644 index cc3f95da860..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 14, "Ca0": 0, "Ca_meas": {"0.0": 0.081520066870384211, "4320.0": 2.8337428652268013, "7560.0": 3.9530051428610888, "21600.0": 1.1807185641508762, "9720.0": 4.0236480913821637, "16200.0": 1.4376034521825525, "20520.0": 1.5413859123725682, "3240.0": 2.7181227248994633, "11880.0": 3.3903617547506242, "14040.0": 2.0159168723544196, "17280.0": 1.0638207528750085, "2160.0": 1.8959521419461316, "5400.0": 2.9705863021885124, "18360.0": 1.0674705545585839, "8640.0": 4.144391992823846, "6480.0": 3.5918471023904477, "1080.0": 1.0113708479421195, "15120.0": 1.6541373379275581, "12960.0": 2.6476139688086877, "19440.0": 1.2312633238777129, "10800.0": 3.8743606114727154}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.089503442052066576, "4320.0": 0.18208896954493076, "7560.0": 0.17697219964055824, "21600.0": 6.8780014679710426, "9720.0": 0.35693081777790492, "16200.0": 3.1977597178279002, "20520.0": 6.1361982627818481, "3240.0": 0.10972700122863582, "11880.0": 0.76070080263615369, "14040.0": 1.5671921543009262, "17280.0": 4.0972632572434122, "2160.0": -0.28322371444225319, "5400.0": -0.079382269802482266, "18360.0": 4.5706426776745905, "8640.0": 0.26888423813019369, "6480.0": 0.1979519809989283, "1080.0": 0.13979150229071807, "15120.0": 2.6867100129129424, "12960.0": 0.99472739483139283, "19440.0": 5.6554142424694902, "10800.0": 0.46709816548507599}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.1139198104669558, "4320.0": 0.99036932502879282, "7560.0": 2.4258194495364482, "21600.0": 7.0446273994379434, "9720.0": 3.7506516413639113, "16200.0": 6.9192013751011503, "20520.0": 7.1555299371654808, "3240.0": 0.66230857796133746, "11880.0": 5.0716652323781499, "14040.0": 7.0971130388695007, "17280.0": 7.4091470358534082, "2160.0": -0.039078609338807413, "5400.0": 1.480378464133409, "18360.0": 7.1741052031883399, "8640.0": 3.4996110541636019, "6480.0": 2.0450173775271647, "1080.0": 0.13728827557251419, "15120.0": 6.7382150794212539, "12960.0": 6.3936782268937753, "19440.0": 7.3298178049407321, "10800.0": 4.6925893428035463}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.43741888919669, "4320.0": 313.81215912108536, "7560.0": 313.22398065446896, "21600.0": 347.95979639817102, "9720.0": 316.38480427739029, "16200.0": 350.46267406552954, "20520.0": 349.96840569755773, "3240.0": 313.32903259260996, "11880.0": 345.61089172333249, "14040.0": 352.83446396320579, "17280.0": 347.23320123776085, "2160.0": 310.02897702317114, "5400.0": 314.70707924235739, "18360.0": 349.8524737596988, "8640.0": 314.13917383134913, "6480.0": 314.15959183349207, "1080.0": 307.97982604287193, "15120.0": 349.00176197155969, "12960.0": 350.41651244789142, "19440.0": 346.1591726550746, "10800.0": 317.90588794308121}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out deleted file mode 100644 index e5245dff3f0..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 2, "Ca0": 0, "Ca_meas": {"12960.0": 2.6833775561963225, "21600.0": 1.1452663167259416, "17280.0": 1.2021595324165524, "19440.0": 1.0621392247792012, "0.0": -0.1316904398446574, "7560.0": 3.544605362880795, "11880.0": 3.1817551426501267, "14040.0": 2.066815570405579, "4320.0": 3.0488618589043432, "15120.0": 1.5896211475539537, "1080.0": 0.8608182979507091, "18360.0": 1.1317484585922248, "8640.0": 3.454602822099547, "2160.0": 1.7479951078246254, "20520.0": 1.2966191801491087, "9720.0": 4.0596636929917285, "6480.0": 3.9085446597134283, "3240.0": 2.5050366860794875, "5400.0": 3.2668528110981576, "10800.0": 4.004828727345138, "16200.0": 1.2860720212507326}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"12960.0": 1.3318254182147888, "21600.0": 6.431089735352747, "17280.0": 3.9711701719678825, "19440.0": 5.321278981143728, "0.0": 0.10325037628575211, "7560.0": 0.10827803632010198, "11880.0": 0.5184920846420157, "14040.0": 1.7974496302186054, "4320.0": 0.03112694971654564, "15120.0": 2.5245584142423207, "1080.0": 0.27315169217241275, "18360.0": 4.587420104936772, "8640.0": 0.1841080751926184, "2160.0": -0.3018405210283593, "20520.0": 6.0086124912796794, "9720.0": 0.08100716578409362, "6480.0": 0.027897809479352567, "3240.0": 0.01973928836607919, "5400.0": 0.02694434057766582, "10800.0": 0.3021977838614568, "16200.0": 3.561159267372319}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"12960.0": 6.358162874770003, "21600.0": 7.190121324797683, "17280.0": 7.302887809357959, "19440.0": 7.249179120248633, "0.0": -0.2300456054526237, "7560.0": 2.5654112157969364, "11880.0": 5.3061363321784025, "14040.0": 6.84009925060659, "4320.0": 0.9644475554073758, "15120.0": 7.261439274435592, "1080.0": 0.2644823572584467, "18360.0": 7.3015136544611305, "8640.0": 3.065189548359326, "2160.0": 0.27122113581760515, "20520.0": 7.162520282520871, "9720.0": 3.701445814227159, "6480.0": 2.0255650533089735, "3240.0": 0.48891328422133334, "5400.0": 1.326135697891304, "10800.0": 4.031252283597449, "16200.0": 7.38249778574694}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"12960.0": 350.10381529626676, "21600.0": 345.04247547236935, "17280.0": 348.24859807524, "19440.0": 346.7076647696431, "0.0": 301.30135373385, "7560.0": 314.7140350191631, "11880.0": 343.608488145403, "14040.0": 352.2902404657956, "4320.0": 312.2349716351422, "15120.0": 351.28330527232634, "1080.0": 307.67178366332996, "18360.0": 347.3517733033304, "8640.0": 313.62143853358026, "2160.0": 310.2674222024707, "20520.0": 348.1662021459614, "9720.0": 314.8081875940964, "6480.0": 312.7396303616638, "3240.0": 310.9258419605079, "5400.0": 312.8448509580561, "10800.0": 317.33866255037736, "16200.0": 349.5494112095972}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out deleted file mode 100644 index a9b013d476f..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 3, "Ca0": 0, "Ca_meas": {"17280.0": 0.39426558381033217, "0.0": -0.25761950998771116, "7560.0": 7.638659217862309, "9720.0": 7.741721088143662, "19440.0": 0.12706787288438182, "16200.0": 0.15060928317089423, "11880.0": 4.534965302380243, "2160.0": 4.47101661859487, "8640.0": 7.562734803826617, "14040.0": 0.8456407304976143, "4320.0": 7.395023123698018, "6480.0": 7.952409415603349, "1080.0": 3.0448009666821947, "18360.0": 0.0754742404427045, "10800.0": 7.223420802364689, "21600.0": 0.181946676125186, "20520.0": 0.195256504023462, "5400.0": 7.844394030136843, "3240.0": 6.031994466757849, "12960.0": 1.813573590958129, "15120.0": 0.30408071219857113}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"17280.0": 10.587758291638856, "0.0": -0.21796531802425348, "7560.0": 0.8680716299725404, "9720.0": 0.9085250272598128, "19440.0": 11.914154788848554, "16200.0": 9.737618534040658, "11880.0": 2.0705302921286064, "2160.0": -0.022903632391514165, "8640.0": 0.5195918959805059, "14040.0": 6.395605356788582, "4320.0": 0.010107836996695638, "6480.0": 0.09956884355869228, "1080.0": -0.21178603534213003, "18360.0": 10.990013628196317, "10800.0": 1.345982231414325, "21600.0": 12.771296192955955, "20520.0": 12.196513048345082, "5400.0": 0.04790148745481311, "3240.0": 0.318546588876358, "12960.0": 4.693433882970767, "15120.0": 8.491695125145553}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 8.731932027503914, "0.0": -0.1375816812387338, "7560.0": 6.8484762842626985, "9720.0": 9.333087689786101, "19440.0": 7.381577268709603, "16200.0": 9.502048821225666, "11880.0": 12.406853134672218, "2160.0": 0.8107944776900446, "8640.0": 8.158484571318013, "14040.0": 11.54445651179274, "4320.0": 2.8119825114954082, "6480.0": 5.520857819630275, "1080.0": 0.18414413253133835, "18360.0": 8.145712620219781, "10800.0": 10.75765409121092, "21600.0": 6.1356948865706356, "20520.0": 6.576247039355788, "5400.0": 3.92591568907661, "3240.0": 2.015632242014947, "12960.0": 12.71320139030468, "15120.0": 10.314039809497785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 345.7556825478521, "0.0": 300.1125978749429, "7560.0": 319.7301093967675, "9720.0": 321.4474947517745, "19440.0": 345.1982881134514, "16200.0": 348.50691612993586, "11880.0": 357.2800598685144, "2160.0": 311.7652063627056, "8640.0": 323.6663990115871, "14040.0": 365.2497105829804, "4320.0": 317.1702037696461, "6480.0": 319.9056594806601, "1080.0": 309.6187369359568, "18360.0": 345.1427626681205, "10800.0": 323.9154289387584, "21600.0": 345.45022165546357, "20520.0": 344.5991879566869, "5400.0": 316.21958050333416, "3240.0": 314.95895736530616, "12960.0": 371.5669986462856, "15120.0": 355.3612054360245}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out deleted file mode 100644 index e702db7d05b..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 4, "Ca0": 0, "Ca_meas": {"17280.0": 1.1816674202534045, "0.0": -0.25414468591124584, "7560.0": 5.914582094251343, "9720.0": 5.185067133371561, "19440.0": 0.5307435290768995, "16200.0": 1.2011215994628408, "11880.0": 3.5778183914967925, "2160.0": 4.254440534150475, "8640.0": 5.473440610227645, "14040.0": 2.1475894664278354, "4320.0": 5.8795707148110266, "6480.0": 6.089523479429854, "1080.0": 2.4339586418303543, "18360.0": 0.545228377126232, "10800.0": 4.946406396626746, "21600.0": 0.3169450590438124, "20520.0": 0.5859997070045333, "5400.0": 6.15901928205937, "3240.0": 5.5559094344993225, "12960.0": 2.476561130612629, "15120.0": 1.35232620260846}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.529641678005382, "0.0": 0.23057916607631007, "7560.0": 1.4209973582182187, "9720.0": 2.662002861314475, "19440.0": 7.612677904908142, "16200.0": 6.236740105453746, "11880.0": 3.6132373498432813, "2160.0": 0.41778377045750303, "8640.0": 2.3031169482702336, "14040.0": 4.857027337132376, "4320.0": 0.21846387467958883, "6480.0": 1.304912741118713, "1080.0": -0.002497120213976349, "18360.0": 7.207560113722179, "10800.0": 3.072350404943197, "21600.0": 8.437070128901182, "20520.0": 7.985790844633096, "5400.0": 0.5552902218354748, "3240.0": 0.4207298617922146, "12960.0": 4.791797355546968, "15120.0": 5.544868662346418}, "alphaj": 0.8, "Cb0": 2, "Vr0": 1, "Cb_meas": {"17280.0": 13.559468310792148, "0.0": 2.1040937779048963, "7560.0": 9.809117250733637, "9720.0": 12.404875593478181, "19440.0": 12.616477055699818, "16200.0": 13.94157106495499, "11880.0": 13.828382570964388, "2160.0": 3.1373485614417618, "8640.0": 11.053587506450443, "14040.0": 14.267859106799012, "4320.0": 5.6037726942190424, "6480.0": 8.499893646580981, "1080.0": 2.2930856900001535, "18360.0": 13.566545045105496, "10800.0": 13.397445458860116, "21600.0": 12.347983697813623, "20520.0": 12.422291796055749, "5400.0": 7.367727360896684, "3240.0": 3.8542518870239824, "12960.0": 14.038139660530316, "15120.0": 14.114308276615096}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.4465785902298, "0.0": 299.6733512720839, "7560.0": 333.04897592093835, "9720.0": 334.48916083151335, "19440.0": 323.5321646227951, "16200.0": 326.84528144052564, "11880.0": 332.77528830002086, "2160.0": 322.2453586799791, "8640.0": 334.2512423463752, "14040.0": 329.05431569837447, "4320.0": 327.99896899102527, "6480.0": 334.3262547857335, "1080.0": 317.32350372398014, "18360.0": 324.97573570445866, "10800.0": 334.23107235994195, "21600.0": 323.6424061352077, "20520.0": 324.00045995355015, "5400.0": 331.45112696032044, "3240.0": 326.33322784448785, "12960.0": 330.4778004752374, "15120.0": 326.8776604963411}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out deleted file mode 100644 index 6c4b1b1d9e0..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 5, "Ca0": 0, "Ca_meas": {"17280.0": 0.5384747196579402, "0.0": -0.14396911833443257, "7560.0": 6.038385982663388, "9720.0": 5.0792329539506, "19440.0": 0.3782801126758533, "16200.0": 1.0619887309834395, "11880.0": 3.6494330494296436, "2160.0": 4.775401751873804, "8640.0": 5.629577532845656, "14040.0": 2.0037718871692265, "4320.0": 5.889802624055117, "6480.0": 6.09724817816528, "1080.0": 2.875851853145854, "18360.0": 0.6780066197547887, "10800.0": 4.859469684381779, "21600.0": 0.3889400954173796, "20520.0": 0.3351378562274788, "5400.0": 6.127222815180268, "3240.0": 5.289726682847115, "12960.0": 2.830845316709853, "15120.0": 1.5312992911111707}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 2, "Tc1": 320, "Cc_meas": {"17280.0": 7.596710556194337, "0.0": 1.6504080367065743, "7560.0": 2.809410585275859, "9720.0": 3.8419183958835132, "19440.0": 8.137782633931637, "16200.0": 6.938967086259325, "11880.0": 5.022162589071362, "2160.0": 2.0515545922033964, "8640.0": 3.506455726732785, "14040.0": 6.010539749263416, "4320.0": 2.2056993474658584, "6480.0": 2.5775763528099858, "1080.0": 2.03522693402577, "18360.0": 8.083917616781594, "10800.0": 4.662851778068136, "21600.0": 9.279674687903626, "20520.0": 8.963676424956157, "5400.0": 2.293408505844697, "3240.0": 1.9216270432789067, "12960.0": 5.637375563057352, "15120.0": 6.3296720972633045}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.831256801432378, "0.0": 0.13194621122245662, "7560.0": 7.965534934436229, "9720.0": 10.908595985103954, "19440.0": 12.408596390398941, "16200.0": 12.975405069340143, "11880.0": 12.710800046234393, "2160.0": 0.9223242691530996, "8640.0": 9.454601468197033, "14040.0": 13.19437793062601, "4320.0": 3.713168763161746, "6480.0": 6.515936097446724, "1080.0": 0.031354105110323494, "18360.0": 12.821094923672087, "10800.0": 11.90520078370877, "21600.0": 11.81429953673305, "20520.0": 12.099271866573613, "5400.0": 4.982941965055916, "3240.0": 2.766378581935415, "12960.0": 13.074621618364043, "15120.0": 12.957819319226212}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 326.8759279818479, "0.0": 300.7736745288814, "7560.0": 333.6674031533901, "9720.0": 333.59854139744135, "19440.0": 324.5264772316974, "16200.0": 325.2454701315101, "11880.0": 332.9849253092768, "2160.0": 322.1940607068012, "8640.0": 331.78378240085084, "14040.0": 328.48981010099453, "4320.0": 327.3883510651506, "6480.0": 330.15101610436426, "1080.0": 318.24994073025096, "18360.0": 323.9527212120804, "10800.0": 333.3006916263996, "21600.0": 322.07065855783964, "20520.0": 324.3518907083261, "5400.0": 331.5429008148077, "3240.0": 324.52116111644654, "12960.0": 329.21899337854876, "15120.0": 328.26179934031467}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out deleted file mode 100644 index c1630902e1a..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 6, "Ca0": 2, "Ca_meas": {"17280.0": 1.1595382970987758, "0.0": 1.9187666718004224, "7560.0": 5.977461039170304, "9720.0": 5.164472215594892, "19440.0": 0.6528636977624275, "16200.0": 1.2106046606700225, "11880.0": 3.3229659243191296, "2160.0": 5.923887627906124, "8640.0": 5.225477003110976, "14040.0": 1.7878931129582107, "4320.0": 6.782806717544953, "6480.0": 6.27323507174512, "1080.0": 4.481633914987097, "18360.0": 0.8866911582721309, "10800.0": 4.481150474336123, "21600.0": 0.2170007972283953, "20520.0": 0.3199825651255196, "5400.0": 6.795886093698936, "3240.0": 6.288606047308427, "12960.0": 2.4509424000990685, "15120.0": 1.506568611506372}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.588306395438309, "0.0": 0.24402401145820712, "7560.0": 1.4036889138374646, "9720.0": 2.4218855673847455, "19440.0": 7.764492558630308, "16200.0": 6.205315919403138, "11880.0": 3.593219427441702, "2160.0": -0.10553376629311664, "8640.0": 1.8628128392103824, "14040.0": 5.027532358914124, "4320.0": 0.2172961549286831, "6480.0": 0.875637414228913, "1080.0": 0.2688503672636328, "18360.0": 7.240866507350995, "10800.0": 3.26514503365032, "21600.0": 8.251445433411781, "20520.0": 7.987953548408583, "5400.0": 0.6458428001299884, "3240.0": 0.3201498579399834, "12960.0": 4.263491165240245, "15120.0": 5.885223860529403}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 13.604962664333257, "0.0": -0.20747860874385013, "7560.0": 10.013416230907982, "9720.0": 12.055665770517061, "19440.0": 13.289064414490856, "16200.0": 13.82240671329648, "11880.0": 13.783622629658064, "2160.0": 1.8287780310400052, "8640.0": 11.17018006067254, "14040.0": 14.064985893141849, "4320.0": 5.072613174963567, "6480.0": 8.597613514631933, "1080.0": 0.3932885074677299, "18360.0": 13.473844971871975, "10800.0": 13.343451941795923, "21600.0": 12.209822751574375, "20520.0": 12.483108442400093, "5400.0": 6.7290370118557545, "3240.0": 3.4163314305947527, "12960.0": 14.296996898861073, "15120.0": 14.543019390786785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.3952638382081, "0.0": 300.9417777707309, "7560.0": 334.57634369064954, "9720.0": 336.13718804612154, "19440.0": 324.10564173791454, "16200.0": 324.97714743647435, "11880.0": 332.29384802281055, "2160.0": 324.243456129639, "8640.0": 335.85007440436317, "14040.0": 329.01187645109906, "4320.0": 331.1961476255781, "6480.0": 333.16262386818596, "1080.0": 319.0632107387995, "18360.0": 322.11836267923206, "10800.0": 332.8894634515628, "21600.0": 323.92451205164855, "20520.0": 323.319714630304, "5400.0": 334.21206737651613, "3240.0": 326.78695915581983, "12960.0": 329.6184998003745, "15120.0": 327.5414299857002}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out deleted file mode 100644 index 6ef879f3a17..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 7, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out deleted file mode 100644 index 6aa9fea17b3..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 8, "Ca0": 0, "Ca_meas": {"0.0": 0.30862088766711671, "4320.0": 6.0491110491659228, "7560.0": 5.7909310485601502, "21600.0": 0.232444399226299, "9720.0": 4.9320449060797475, "16200.0": 0.97134242753331668, "20520.0": 0.42847724332841963, "3240.0": 5.6988320807198498, "11880.0": 3.3235733576868514, "14040.0": 1.9846460628194049, "17280.0": 0.87715206210722585, "2160.0": 4.615346351863904, "5400.0": 6.384056703029386, "18360.0": 0.41688144324552118, "8640.0": 5.4121173109702099, "6480.0": 6.0660731346226324, "1080.0": 2.8379509025410488, "15120.0": 0.98831570466285279, "12960.0": 2.2167483934357417, "19440.0": 0.46284950985984985, "10800.0": 4.7377220491627412}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.050833820035522316, "4320.0": 0.21055066883154505, "7560.0": 1.3851246436045646, "21600.0": 7.3672985895890362, "9720.0": 2.0100502709379842, "16200.0": 5.1793087406376159, "20520.0": 6.840381847429823, "3240.0": 0.13411276227648503, "11880.0": 3.3052152545385454, "14040.0": 3.9431305823279708, "17280.0": 5.7290141848801586, "2160.0": 0.16719633749951002, "5400.0": 0.49872603502453117, "18360.0": 6.1508540969551078, "8640.0": 1.4737312345987361, "6480.0": 0.69437977126769512, "1080.0": -0.0093978134715377304, "15120.0": 4.9151661032041298, "12960.0": 4.0623539766149843, "19440.0": 6.3058400571478561, "10800.0": 2.820347355873587}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.037166728983599892, "4320.0": 3.5183932586092856, "7560.0": 8.916682434428397, "21600.0": 11.534104657089006, "9720.0": 11.087958791247285, "16200.0": 13.02597376112109, "20520.0": 11.639999923137731, "3240.0": 2.346958004261503, "11880.0": 12.049613604010537, "14040.0": 12.906738918465997, "17280.0": 12.867691165140879, "2160.0": 1.3313958783841602, "5400.0": 5.3650409213472221, "18360.0": 12.405763004965722, "8640.0": 9.5635832344445717, "6480.0": 7.0954049721214671, "1080.0": 0.40883709280782765, "15120.0": 12.971506554625082, "12960.0": 12.829158718434032, "19440.0": 11.946615137583075, "10800.0": 11.373799750334223}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.55141264078583, "4320.0": 329.42497918063066, "7560.0": 333.82602046942475, "21600.0": 323.68642879192487, "9720.0": 332.94208820576767, "16200.0": 325.82299128298814, "20520.0": 325.19753703643721, "3240.0": 329.66504941755875, "11880.0": 332.29546982118751, "14040.0": 326.51837436850099, "17280.0": 326.51851506890586, "2160.0": 323.70134945698589, "5400.0": 328.6805843225718, "18360.0": 324.79832692054578, "8640.0": 331.94068007914785, "6480.0": 332.75141896044545, "1080.0": 318.90722718736015, "15120.0": 325.85289150843209, "12960.0": 327.72250161440121, "19440.0": 325.17198606848808, "10800.0": 334.1255807822717}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out deleted file mode 100644 index 627f92b1f83..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 9, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv deleted file mode 100644 index 79f03e07dcd..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv +++ /dev/null @@ -1,1009 +0,0 @@ -,k1,k2,E1,E2,obj -0,4,40,29000,38000,667.4023645794207 -1,4,40,29000,38500,665.8312183437167 -2,4,40,29000,39000,672.7539769993407 -3,4,40,29000,39500,684.9503752463216 -4,4,40,29000,40000,699.985589093255 -5,4,40,29000,40500,716.1241770970677 -6,4,40,29000,41000,732.2023201586336 -7,4,40,29000,41500,747.4931745925483 -8,4,40,29500,38000,907.4405527163311 -9,4,40,29500,38500,904.2229271927299 -10,4,40,29500,39000,907.6942345285257 -11,4,40,29500,39500,915.4570013614677 -12,4,40,29500,40000,925.65401444575 -13,4,40,29500,40500,936.9348578520337 -14,4,40,29500,41000,948.3759339765711 -15,4,40,29500,41500,959.386491783636 -16,4,40,30000,38000,1169.8685711377334 -17,4,40,30000,38500,1166.2211505723928 -18,4,40,30000,39000,1167.702295374574 -19,4,40,30000,39500,1172.5517020611685 -20,4,40,30000,40000,1179.3820406408263 -21,4,40,30000,40500,1187.1698633839655 -22,4,40,30000,41000,1195.2047840919602 -23,4,40,30000,41500,1203.0241101248102 -24,4,40,30500,38000,1445.9591944684807 -25,4,40,30500,38500,1442.6632745483 -26,4,40,30500,39000,1443.1982444457385 -27,4,40,30500,39500,1446.2833842279929 -28,4,40,30500,40000,1450.9012120934779 -29,4,40,30500,40500,1456.295140290636 -30,4,40,30500,41000,1461.9350767569827 -31,4,40,30500,41500,1467.4715014446226 -32,4,40,31000,38000,1726.8744994061449 -33,4,40,31000,38500,1724.2679845375048 -34,4,40,31000,39000,1724.4550886870552 -35,4,40,31000,39500,1726.5124587129135 -36,4,40,31000,40000,1729.7061680616455 -37,4,40,31000,40500,1733.48893482641 -38,4,40,31000,41000,1737.4753558920438 -39,4,40,31000,41500,1741.4093763605517 -40,4,40,31500,38000,2004.1978135112938 -41,4,40,31500,38500,2002.2807839860222 -42,4,40,31500,39000,2002.3676405166086 -43,4,40,31500,39500,2003.797808439923 -44,4,40,31500,40000,2006.048051591001 -45,4,40,31500,40500,2008.7281679153625 -46,4,40,31500,41000,2011.5626384878237 -47,4,40,31500,41500,2014.3675286347284 -48,4,80,29000,38000,845.8197358579285 -49,4,80,29000,38500,763.5039795545781 -50,4,80,29000,39000,709.8529964173656 -51,4,80,29000,39500,679.4215539491266 -52,4,80,29000,40000,666.4876088521157 -53,4,80,29000,40500,665.978271760966 -54,4,80,29000,41000,673.7240200504901 -55,4,80,29000,41500,686.4763909417914 -56,4,80,29500,38000,1042.519415429413 -57,4,80,29500,38500,982.8097210678039 -58,4,80,29500,39000,942.2990207573541 -59,4,80,29500,39500,917.9550916645245 -60,4,80,29500,40000,906.3116029967189 -61,4,80,29500,40500,904.0326666308792 -62,4,80,29500,41000,908.1964630052729 -63,4,80,29500,41500,916.4222043837499 -64,4,80,30000,38000,1271.1030403496538 -65,4,80,30000,38500,1227.7527550544085 -66,4,80,30000,39000,1197.433957624904 -67,4,80,30000,39500,1178.447676126182 -68,4,80,30000,40000,1168.645219243497 -69,4,80,30000,40500,1165.7995210546096 -70,4,80,30000,41000,1167.8586496250396 -71,4,80,30000,41500,1173.0949214020527 -72,4,80,30500,38000,1520.8220402652044 -73,4,80,30500,38500,1489.2563260709424 -74,4,80,30500,39000,1466.8099189128857 -75,4,80,30500,39500,1452.4352624958806 -76,4,80,30500,40000,1444.7074679423818 -77,4,80,30500,40500,1442.0820578624343 -78,4,80,30500,41000,1443.099006489627 -79,4,80,30500,41500,1446.5106517200784 -80,4,80,31000,38000,1781.149136032395 -81,4,80,31000,38500,1758.2414369536502 -82,4,80,31000,39000,1741.891639711003 -83,4,80,31000,39500,1731.358661496594 -84,4,80,31000,40000,1725.6231647999593 -85,4,80,31000,40500,1723.5757174297378 -86,4,80,31000,41000,1724.1680229486278 -87,4,80,31000,41500,1726.5050840601884 -88,4,80,31500,38000,2042.8335948845602 -89,4,80,31500,38500,2026.3067503042414 -90,4,80,31500,39000,2014.5720701940838 -91,4,80,31500,39500,2007.0463766643977 -92,4,80,31500,40000,2002.9647983728314 -93,4,80,31500,40500,2001.5163951989875 -94,4,80,31500,41000,2001.9474217001339 -95,4,80,31500,41500,2003.6204088755821 -96,4,120,29000,38000,1176.0713512305115 -97,4,120,29000,38500,1016.8213383282462 -98,4,120,29000,39000,886.0136231565133 -99,4,120,29000,39500,789.0101180066036 -100,4,120,29000,40000,724.5420056133441 -101,4,120,29000,40500,686.6877602625062 -102,4,120,29000,41000,668.8129085873959 -103,4,120,29000,41500,665.1167761036883 -104,4,120,29500,38000,1263.887274509128 -105,4,120,29500,38500,1155.6528408872423 -106,4,120,29500,39000,1066.393539894248 -107,4,120,29500,39500,998.9931006471243 -108,4,120,29500,40000,952.36314487701 -109,4,120,29500,40500,923.4000293372077 -110,4,120,29500,41000,908.407361383214 -111,4,120,29500,41500,903.8136176328255 -112,4,120,30000,38000,1421.1418235449091 -113,4,120,30000,38500,1347.114022652679 -114,4,120,30000,39000,1285.686103704643 -115,4,120,30000,39500,1238.2456448658272 -116,4,120,30000,40000,1204.3526810790904 -117,4,120,30000,40500,1182.4272879027071 -118,4,120,30000,41000,1170.3447810121902 -119,4,120,30000,41500,1165.8422968073423 -120,4,120,30500,38000,1625.5588911535713 -121,4,120,30500,38500,1573.5546642859429 -122,4,120,30500,39000,1530.1592840718379 -123,4,120,30500,39500,1496.2087139473604 -124,4,120,30500,40000,1471.525855239756 -125,4,120,30500,40500,1455.2084749904016 -126,4,120,30500,41000,1445.9160840082027 -127,4,120,30500,41500,1442.1255377330835 -128,4,120,31000,38000,1855.8467211183756 -129,4,120,31000,38500,1818.4368412235558 -130,4,120,31000,39000,1787.25956706785 -131,4,120,31000,39500,1762.8169908546402 -132,4,120,31000,40000,1744.9825741661596 -133,4,120,31000,40500,1733.136625016882 -134,4,120,31000,41000,1726.3352245899828 -135,4,120,31000,41500,1723.492199933745 -136,4,120,31500,38000,2096.6479813687533 -137,4,120,31500,38500,2069.3606691038876 -138,4,120,31500,39000,2046.792043575205 -139,4,120,31500,39500,2029.2128703900223 -140,4,120,31500,40000,2016.4664599897606 -141,4,120,31500,40500,2008.054814885348 -142,4,120,31500,41000,2003.2622557140814 -143,4,120,31500,41500,2001.289784483679 -144,7,40,29000,38000,149.32898706737052 -145,7,40,29000,38500,161.04814413969586 -146,7,40,29000,39000,187.87801343005242 -147,7,40,29000,39500,223.00789161520424 -148,7,40,29000,40000,261.66779887964003 -149,7,40,29000,40500,300.676316191238 -150,7,40,29000,41000,338.04021206995765 -151,7,40,29000,41500,372.6191631389286 -152,7,40,29500,38000,276.6495061185777 -153,7,40,29500,38500,282.1304583501965 -154,7,40,29500,39000,300.91417483065254 -155,7,40,29500,39500,327.24304394350395 -156,7,40,29500,40000,357.0561976596432 -157,7,40,29500,40500,387.61662064170207 -158,7,40,29500,41000,417.1836349752378 -159,7,40,29500,41500,444.73705844573243 -160,7,40,30000,38000,448.0380830353589 -161,7,40,30000,38500,448.8094536459122 -162,7,40,30000,39000,460.77530593327293 -163,7,40,30000,39500,479.342874472736 -164,7,40,30000,40000,501.20694459059405 -165,7,40,30000,40500,524.0971649678811 -166,7,40,30000,41000,546.539334134893 -167,7,40,30000,41500,567.6447156158981 -168,7,40,30500,38000,657.9909416906933 -169,7,40,30500,38500,655.7465129488842 -170,7,40,30500,39000,662.5420970804985 -171,7,40,30500,39500,674.8914651553109 -172,7,40,30500,40000,690.2111920703564 -173,7,40,30500,40500,706.6833639709198 -174,7,40,30500,41000,723.0994507096715 -175,7,40,30500,41500,738.7096013891406 -176,7,40,31000,38000,899.1769906655776 -177,7,40,31000,38500,895.4391505892945 -178,7,40,31000,39000,898.7695629120826 -179,7,40,31000,39500,906.603316771593 -180,7,40,31000,40000,916.9811481373996 -181,7,40,31000,40500,928.4913367709245 -182,7,40,31000,41000,940.1744934710283 -183,7,40,31000,41500,951.4199286075984 -184,7,40,31500,38000,1163.093373675207 -185,7,40,31500,38500,1159.0457727559028 -186,7,40,31500,39000,1160.3831770028223 -187,7,40,31500,39500,1165.2451698296604 -188,7,40,31500,40000,1172.1768190340001 -189,7,40,31500,40500,1180.1105659428963 -190,7,40,31500,41000,1188.3083929833688 -191,7,40,31500,41500,1196.29112579565 -192,7,80,29000,38000,514.0332369183081 -193,7,80,29000,38500,329.3645784712966 -194,7,80,29000,39000,215.73000998706416 -195,7,80,29000,39500,162.37338399591852 -196,7,80,29000,40000,149.8401793263549 -197,7,80,29000,40500,162.96125998112578 -198,7,80,29000,41000,191.173279165834 -199,7,80,29000,41500,227.2781971491003 -200,7,80,29500,38000,623.559246695578 -201,7,80,29500,38500,448.60620511421484 -202,7,80,29500,39000,344.21940687907573 -203,7,80,29500,39500,292.9758707105001 -204,7,80,29500,40000,277.07670134364804 -205,7,80,29500,40500,283.5158840045542 -206,7,80,29500,41000,303.33951582820265 -207,7,80,29500,41500,330.43357046741954 -208,7,80,30000,38000,732.5907387079073 -209,7,80,30000,38500,593.1926567994672 -210,7,80,30000,39000,508.5638538704666 -211,7,80,30000,39500,464.47881763522037 -212,7,80,30000,40000,448.0394620671692 -213,7,80,30000,40500,449.64309860415494 -214,7,80,30000,41000,462.4490598612332 -215,7,80,30000,41500,481.6323506247537 -216,7,80,30500,38000,871.1163930229344 -217,7,80,30500,38500,771.1320563649375 -218,7,80,30500,39000,707.8872660015606 -219,7,80,30500,39500,672.6612145133173 -220,7,80,30500,40000,657.4974157809264 -221,7,80,30500,40500,656.0835852491216 -222,7,80,30500,41000,663.6006958125331 -223,7,80,30500,41500,676.460675405631 -224,7,80,31000,38000,1053.1852617390061 -225,7,80,31000,38500,984.3647109805877 -226,7,80,31000,39000,938.6158531749268 -227,7,80,31000,39500,911.4268280093535 -228,7,80,31000,40000,898.333365348419 -229,7,80,31000,40500,895.3996527486954 -230,7,80,31000,41000,899.3556288533885 -231,7,80,31000,41500,907.6180684887955 -232,7,80,31500,38000,1274.2255948763498 -233,7,80,31500,38500,1226.5236809533717 -234,7,80,31500,39000,1193.4538731398666 -235,7,80,31500,39500,1172.8105398345213 -236,7,80,31500,40000,1162.0692230240734 -237,7,80,31500,40500,1158.7461521476607 -238,7,80,31500,41000,1160.6173577210805 -239,7,80,31500,41500,1165.840315694716 -240,7,120,29000,38000,1325.2409732290193 -241,7,120,29000,38500,900.8063148840154 -242,7,120,29000,39000,629.9300352098937 -243,7,120,29000,39500,413.81648033893424 -244,7,120,29000,40000,257.3116751690404 -245,7,120,29000,40500,177.89217179438947 -246,7,120,29000,41000,151.58366848473491 -247,7,120,29000,41500,157.56967437251706 -248,7,120,29500,38000,1211.2807882170853 -249,7,120,29500,38500,956.936161969002 -250,7,120,29500,39000,753.3050086992201 -251,7,120,29500,39500,528.2452647799327 -252,7,120,29500,40000,382.62610532894917 -253,7,120,29500,40500,308.44199089882375 -254,7,120,29500,41000,280.3893024671524 -255,7,120,29500,41500,280.4028092582749 -256,7,120,30000,38000,1266.5740351143413 -257,7,120,30000,38500,1084.3028700477778 -258,7,120,30000,39000,834.2392498526193 -259,7,120,30000,39500,650.7560171314304 -260,7,120,30000,40000,537.7846910878052 -261,7,120,30000,40500,477.3001078155485 -262,7,120,30000,41000,451.6865380286754 -263,7,120,30000,41500,448.14911508024613 -264,7,120,30500,38000,1319.6603196780936 -265,7,120,30500,38500,1102.3027489012372 -266,7,120,30500,39000,931.2523583659847 -267,7,120,30500,39500,807.0833484596384 -268,7,120,30500,40000,727.4852710400268 -269,7,120,30500,40500,682.1437030344305 -270,7,120,30500,41000,660.7859329989657 -271,7,120,30500,41500,655.6001132492668 -272,7,120,31000,38000,1330.5306924865326 -273,7,120,31000,38500,1195.9190861202942 -274,7,120,31000,39000,1086.0328080422887 -275,7,120,31000,39500,1005.4160637517409 -276,7,120,31000,40000,951.2021706290612 -277,7,120,31000,40500,918.1457644271304 -278,7,120,31000,41000,901.0511005554887 -279,7,120,31000,41500,895.4599964465793 -280,7,120,31500,38000,1447.8365822059013 -281,7,120,31500,38500,1362.3417347939844 -282,7,120,31500,39000,1292.382727215108 -283,7,120,31500,39500,1239.1826828976662 -284,7,120,31500,40000,1201.6474412465277 -285,7,120,31500,40500,1177.5235955796813 -286,7,120,31500,41000,1164.1761722345295 -287,7,120,31500,41500,1158.9997785002718 -288,10,40,29000,38000,33.437068437082054 -289,10,40,29000,38500,58.471249815534996 -290,10,40,29000,39000,101.41937628542912 -291,10,40,29000,39500,153.80690200519626 -292,10,40,29000,40000,209.66451461551316 -293,10,40,29000,40500,265.03070792175197 -294,10,40,29000,41000,317.46079310177566 -295,10,40,29000,41500,365.59950388342645 -296,10,40,29500,38000,70.26818405688635 -297,10,40,29500,38500,87.96463718548947 -298,10,40,29500,39000,122.58188233160993 -299,10,40,29500,39500,166.2478945807132 -300,10,40,29500,40000,213.48669617414316 -301,10,40,29500,40500,260.67953961944477 -302,10,40,29500,41000,305.5877041218316 -303,10,40,29500,41500,346.95612213021155 -304,10,40,30000,38000,153.67588703371362 -305,10,40,30000,38500,164.07504103479005 -306,10,40,30000,39000,190.0800160661499 -307,10,40,30000,39500,224.61382980242837 -308,10,40,30000,40000,262.79232847382445 -309,10,40,30000,40500,301.38687703450415 -310,10,40,30000,41000,338.38536686093164 -311,10,40,30000,41500,372.6399011703545 -312,10,40,30500,38000,284.2936286531718 -313,10,40,30500,38500,288.4690608277705 -314,10,40,30500,39000,306.44667517621144 -315,10,40,30500,39500,332.20122250191986 -316,10,40,30500,40000,361.5566690083291 -317,10,40,30500,40500,391.72755224929614 -318,10,40,30500,41000,420.95317535960476 -319,10,40,30500,41500,448.2049230608669 -320,10,40,31000,38000,459.03140021766137 -321,10,40,31000,38500,458.71477027519967 -322,10,40,31000,39000,469.9910751800656 -323,10,40,31000,39500,488.05850105225426 -324,10,40,31000,40000,509.5204701455629 -325,10,40,31000,40500,532.0674969691778 -326,10,40,31000,41000,554.2088430693509 -327,10,40,31000,41500,575.0485839499048 -328,10,40,31500,38000,672.2476845983564 -329,10,40,31500,38500,669.2240508488649 -330,10,40,31500,39000,675.4956226836405 -331,10,40,31500,39500,687.447764319295 -332,10,40,31500,40000,702.4395430742891 -333,10,40,31500,40500,718.6279487347668 -334,10,40,31500,41000,734.793684592168 -335,10,40,31500,41500,750.1821072409286 -336,10,80,29000,38000,387.7617282731497 -337,10,80,29000,38500,195.33642612593002 -338,10,80,29000,39000,82.7306931465102 -339,10,80,29000,39500,35.13436471793541 -340,10,80,29000,40000,33.521138659248706 -341,10,80,29000,40500,61.47395975053128 -342,10,80,29000,41000,106.71403229340167 -343,10,80,29000,41500,160.56068704487473 -344,10,80,29500,38000,459.63404601804103 -345,10,80,29500,38500,258.7453720995899 -346,10,80,29500,39000,135.96435731320256 -347,10,80,29500,39500,80.2685095017944 -348,10,80,29500,40000,70.86302366453106 -349,10,80,29500,40500,90.43203026480438 -350,10,80,29500,41000,126.7844695901737 -351,10,80,29500,41500,171.63682876805044 -352,10,80,30000,38000,564.1463320344325 -353,10,80,30000,38500,360.75718124523866 -354,10,80,30000,39000,231.70119191254307 -355,10,80,30000,39500,170.74752201483128 -356,10,80,30000,40000,154.7149036950422 -357,10,80,30000,40500,166.10596450541493 -358,10,80,30000,41000,193.3351721194443 -359,10,80,30000,41500,228.78394172417038 -360,10,80,30500,38000,689.6797223218513 -361,10,80,30500,38500,484.8023695265838 -362,10,80,30500,39000,363.5979340028588 -363,10,80,30500,39500,304.67857102688225 -364,10,80,30500,40000,285.29210000833734 -365,10,80,30500,40500,290.0135917456113 -366,10,80,30500,41000,308.8672169492536 -367,10,80,30500,41500,335.3210332569182 -368,10,80,31000,38000,789.946106942773 -369,10,80,31000,38500,625.7722360026959 -370,10,80,31000,39000,528.6063264942235 -371,10,80,31000,39500,478.6863763478618 -372,10,80,31000,40000,459.5026243189753 -373,10,80,31000,40500,459.6982093164963 -374,10,80,31000,41000,471.6790024321937 -375,10,80,31000,41500,490.3034492109124 -376,10,80,31500,38000,912.3540488244158 -377,10,80,31500,38500,798.2135101409633 -378,10,80,31500,39000,727.746684419146 -379,10,80,31500,39500,689.0119464356724 -380,10,80,31500,40000,672.0757202772029 -381,10,80,31500,40500,669.678339553036 -382,10,80,31500,41000,676.5761221409929 -383,10,80,31500,41500,688.9934449650118 -384,10,120,29000,38000,1155.1165164624408 -385,10,120,29000,38500,840.2641727088946 -386,10,120,29000,39000,506.9102636732852 -387,10,120,29000,39500,265.5278912452038 -388,10,120,29000,40000,116.39516513179322 -389,10,120,29000,40500,45.2088092745619 -390,10,120,29000,41000,30.22267557153353 -391,10,120,29000,41500,51.06063746392809 -392,10,120,29500,38000,1343.7868459826054 -393,10,120,29500,38500,977.9852373227346 -394,10,120,29500,39000,594.632756549817 -395,10,120,29500,39500,346.2478773329187 -396,10,120,29500,40000,180.23082247413407 -397,10,120,29500,40500,95.81649989178923 -398,10,120,29500,41000,71.0837801649128 -399,10,120,29500,41500,82.84289818279714 -400,10,120,30000,38000,1532.9333545384934 -401,10,120,30000,38500,1012.2223350568845 -402,10,120,30000,39000,688.4884716222766 -403,10,120,30000,39500,464.6206903113392 -404,10,120,30000,40000,283.5644748300334 -405,10,120,30000,40500,190.27593217865416 -406,10,120,30000,41000,158.0192279691727 -407,10,120,30000,41500,161.3611926772337 -408,10,120,30500,38000,1349.3785399811063 -409,10,120,30500,38500,1014.785480110738 -410,10,120,30500,39000,843.0316833766408 -411,10,120,30500,39500,589.4543896730125 -412,10,120,30500,40000,412.3358512291996 -413,10,120,30500,40500,324.11715620464133 -414,10,120,30500,41000,290.17588242984766 -415,10,120,30500,41500,287.56857384673356 -416,10,120,31000,38000,1328.0973931040146 -417,10,120,31000,38500,1216.5659656437845 -418,10,120,31000,39000,928.4831767181619 -419,10,120,31000,39500,700.3115484040329 -420,10,120,31000,40000,565.0876352458171 -421,10,120,31000,40500,494.44016026435037 -422,10,120,31000,41000,464.38005437182983 -423,10,120,31000,41500,458.7614573733091 -424,10,120,31500,38000,1473.1154650008834 -425,10,120,31500,38500,1195.943614951571 -426,10,120,31500,39000,990.2486604382486 -427,10,120,31500,39500,843.1390407497395 -428,10,120,31500,40000,751.2746391170706 -429,10,120,31500,40500,700.215375503209 -430,10,120,31500,41000,676.1585052687219 -431,10,120,31500,41500,669.5907920932743 -432,13,40,29000,38000,49.96352152045025 -433,13,40,29000,38500,83.75104994958261 -434,13,40,29000,39000,136.8176091795391 -435,13,40,29000,39500,199.91486685466407 -436,13,40,29000,40000,266.4367154860076 -437,13,40,29000,40500,331.97224579940524 -438,13,40,29000,41000,393.8001583706036 -439,13,40,29000,41500,450.42425363084493 -440,13,40,29500,38000,29.775721038786923 -441,13,40,29500,38500,57.37673742631121 -442,13,40,29500,39000,103.49161398239501 -443,13,40,29500,39500,159.3058253852367 -444,13,40,29500,40000,218.60083223764073 -445,13,40,29500,40500,277.2507278183831 -446,13,40,29500,41000,332.7141278886951 -447,13,40,29500,41500,383.58832292300576 -448,13,40,30000,38000,47.72263852005472 -449,13,40,30000,38500,68.07581028940402 -450,13,40,30000,39000,106.13974628945516 -451,13,40,30000,39500,153.58449949683063 -452,13,40,30000,40000,204.62393623358633 -453,13,40,30000,40500,255.44513025602419 -454,13,40,30000,41000,303.69954914051766 -455,13,40,30000,41500,348.0803709720354 -456,13,40,30500,38000,110.9331168284094 -457,13,40,30500,38500,123.63361262704746 -458,13,40,30500,39000,153.02654433825705 -459,13,40,30500,39500,191.40769947472756 -460,13,40,30500,40000,233.503841403055 -461,13,40,30500,40500,275.8557790922913 -462,13,40,30500,41000,316.32529882763697 -463,13,40,30500,41500,353.7060432094809 -464,13,40,31000,38000,221.90608823073939 -465,13,40,31000,38500,227.67026441593657 -466,13,40,31000,39000,248.62107049869064 -467,13,40,31000,39500,277.9507605389158 -468,13,40,31000,40000,311.0267471957685 -469,13,40,31000,40500,344.8024031161673 -470,13,40,31000,41000,377.3761144228052 -471,13,40,31000,41500,407.6529635071056 -472,13,40,31500,38000,378.8738382757093 -473,13,40,31500,38500,379.39748335944216 -474,13,40,31500,39000,393.01223361732553 -475,13,40,31500,39500,414.10238059122855 -476,13,40,31500,40000,438.8024282436204 -477,13,40,31500,40500,464.5348067190265 -478,13,40,31500,41000,489.6621039898805 -479,13,40,31500,41500,513.2163939332803 -480,13,80,29000,38000,364.387588581215 -481,13,80,29000,38500,184.2902007673634 -482,13,80,29000,39000,81.57192155036655 -483,13,80,29000,39500,42.54811210095659 -484,13,80,29000,40000,49.897338772663076 -485,13,80,29000,40500,87.84229516509882 -486,13,80,29000,41000,143.85451969447664 -487,13,80,29000,41500,208.71467984917848 -488,13,80,29500,38000,382.5794635435733 -489,13,80,29500,38500,188.38619353711718 -490,13,80,29500,39000,75.75749359688277 -491,13,80,29500,39500,29.27891251986562 -492,13,80,29500,40000,29.794874961934568 -493,13,80,29500,40500,60.654888662698205 -494,13,80,29500,41000,109.25801388824325 -495,13,80,29500,41500,166.6311093454692 -496,13,80,30000,38000,448.97795526074816 -497,13,80,30000,38500,238.44530107604737 -498,13,80,30000,39000,112.34545890264337 -499,13,80,30000,39500,56.125871791222835 -500,13,80,30000,40000,48.29987461781518 -501,13,80,30000,40500,70.7900626637678 -502,13,80,30000,41000,110.76865376691964 -503,13,80,30000,41500,159.50197316936024 -504,13,80,30500,38000,547.7818730461195 -505,13,80,30500,38500,332.92604070423494 -506,13,80,30500,39000,193.80760050280742 -507,13,80,30500,39500,128.3457644087917 -508,13,80,30500,40000,112.23915895822442 -509,13,80,30500,40500,125.96369396512564 -510,13,80,30500,41000,156.67918617660013 -511,13,80,30500,41500,196.05195109523765 -512,13,80,31000,38000,682.8591931963246 -513,13,80,31000,38500,457.56562267948556 -514,13,80,31000,39000,313.6380169123524 -515,13,80,31000,39500,245.13531819580908 -516,13,80,31000,40000,223.54473391202873 -517,13,80,31000,40500,229.60752111202834 -518,13,80,31000,41000,251.42377424735136 -519,13,80,31000,41500,281.48720903016886 -520,13,80,31500,38000,807.925638050234 -521,13,80,31500,38500,588.686585641994 -522,13,80,31500,39000,464.0488586698228 -523,13,80,31500,39500,402.69214492641095 -524,13,80,31500,40000,380.13626165363934 -525,13,80,31500,40500,380.8064948609387 -526,13,80,31500,41000,395.05186915919086 -527,13,80,31500,41500,416.70193045600774 -528,13,120,29000,38000,1068.8279454397398 -529,13,120,29000,38500,743.0012805963486 -530,13,120,29000,39000,451.2538301167544 -531,13,120,29000,39500,235.4154251166075 -532,13,120,29000,40000,104.73720814447498 -533,13,120,29000,40500,46.91983990671749 -534,13,120,29000,41000,42.81092192562316 -535,13,120,29000,41500,74.33530639171506 -536,13,120,29500,38000,1133.1178848710972 -537,13,120,29500,38500,824.0745323788527 -538,13,120,29500,39000,499.10867111401996 -539,13,120,29500,39500,256.1626809904186 -540,13,120,29500,40000,107.68599585294751 -541,13,120,29500,40500,38.18533662516749 -542,13,120,29500,41000,25.499608203619154 -543,13,120,29500,41500,49.283537699300375 -544,13,120,30000,38000,1292.409871290162 -545,13,120,30000,38500,994.669572829704 -546,13,120,30000,39000,598.9783697712826 -547,13,120,30000,39500,327.47348408537925 -548,13,120,30000,40000,156.82634841081907 -549,13,120,30000,40500,71.30833688875883 -550,13,120,30000,41000,47.72389750130817 -551,13,120,30000,41500,62.1982461882982 -552,13,120,30500,38000,1585.8797221278146 -553,13,120,30500,38500,1144.66688416451 -554,13,120,30500,39000,692.6651441690645 -555,13,120,30500,39500,441.98837639874046 -556,13,120,30500,40000,251.56311435857728 -557,13,120,30500,40500,149.79670413140468 -558,13,120,30500,41000,115.52645596043719 -559,13,120,30500,41500,120.44019473389324 -560,13,120,31000,38000,1702.7625866892163 -561,13,120,31000,38500,1071.7854750250656 -562,13,120,31000,39000,807.8943299034604 -563,13,120,31000,39500,588.672223513561 -564,13,120,31000,40000,376.44658358671404 -565,13,120,31000,40500,269.2159719426485 -566,13,120,31000,41000,229.41660529009877 -567,13,120,31000,41500,226.78274707181976 -568,13,120,31500,38000,1331.3523701291767 -569,13,120,31500,38500,1151.2055268669133 -570,13,120,31500,39000,1006.811285091974 -571,13,120,31500,39500,702.0053094629535 -572,13,120,31500,40000,515.9081891614829 -573,13,120,31500,40500,423.8652275555525 -574,13,120,31500,41000,386.4939696097151 -575,13,120,31500,41500,379.8118453367429 -576,16,40,29000,38000,106.1025746852808 -577,16,40,29000,38500,145.32590128581407 -578,16,40,29000,39000,204.74804378224422 -579,16,40,29000,39500,274.6339266648551 -580,16,40,29000,40000,347.9667393938497 -581,16,40,29000,40500,420.03753452490974 -582,16,40,29000,41000,487.9353932879741 -583,16,40,29000,41500,550.0623063219693 -584,16,40,29500,38000,54.65040870471303 -585,16,40,29500,38500,88.94089091627293 -586,16,40,29500,39000,142.72223808288405 -587,16,40,29500,39500,206.63598763907422 -588,16,40,29500,40000,273.99851593521134 -589,16,40,29500,40500,340.34861536649436 -590,16,40,29500,41000,402.935270882596 -591,16,40,29500,41500,460.2471155081633 -592,16,40,30000,38000,29.788548081995298 -593,16,40,30000,38500,57.96323252610644 -594,16,40,30000,39000,104.92815906834525 -595,16,40,30000,39500,161.71867032726158 -596,16,40,30000,40000,222.01677586338877 -597,16,40,30000,40500,281.6349465235367 -598,16,40,30000,41000,337.99683241119567 -599,16,40,30000,41500,389.68271710858414 -600,16,40,30500,38000,42.06569536892785 -601,16,40,30500,38500,62.95145274276575 -602,16,40,30500,39000,101.93860830594608 -603,16,40,30500,39500,150.47910837525734 -604,16,40,30500,40000,202.65388851823258 -605,16,40,30500,40500,254.5724108541227 -606,16,40,30500,41000,303.84403622726694 -607,16,40,30500,41500,349.1422884543064 -608,16,40,31000,38000,99.21707896667829 -609,16,40,31000,38500,112.24153596941301 -610,16,40,31000,39000,142.5186177618655 -611,16,40,31000,39500,182.02836955332134 -612,16,40,31000,40000,225.3201896575212 -613,16,40,31000,40500,268.83705389232614 -614,16,40,31000,41000,310.3895932135811 -615,16,40,31000,41500,348.7480165565453 -616,16,40,31500,38000,204.30418825821732 -617,16,40,31500,38500,210.0759235359138 -618,16,40,31500,39000,231.7643258544752 -619,16,40,31500,39500,262.1512494310348 -620,16,40,31500,40000,296.3864127264238 -621,16,40,31500,40500,331.30743171999035 -622,16,40,31500,41000,364.95322314895554 -623,16,40,31500,41500,396.20142191205844 -624,16,80,29000,38000,399.5975649320935 -625,16,80,29000,38500,225.6318269911425 -626,16,80,29000,39000,127.97354075513151 -627,16,80,29000,39500,93.73584101549991 -628,16,80,29000,40000,106.43084032022394 -629,16,80,29000,40500,150.51245762256931 -630,16,80,29000,41000,213.24213500046466 -631,16,80,29000,41500,285.0426423013882 -632,16,80,29500,38000,371.37706087096393 -633,16,80,29500,38500,189.77150413822454 -634,16,80,29500,39000,86.22375488959844 -635,16,80,29500,39500,46.98714814001572 -636,16,80,29500,40000,54.596900621760675 -637,16,80,29500,40500,93.12033833747024 -638,16,80,29500,41000,149.89341227947025 -639,16,80,29500,41500,215.5937000584367 -640,16,80,30000,38000,388.43657991253195 -641,16,80,30000,38500,190.77121362008674 -642,16,80,30000,39000,76.28535232335287 -643,16,80,30000,39500,29.152860363695716 -644,16,80,30000,40000,29.820972887404942 -645,16,80,30000,40500,61.320203047752464 -646,16,80,30000,41000,110.82086782062603 -647,16,80,30000,41500,169.197767615573 -648,16,80,30500,38000,458.8964339917103 -649,16,80,30500,38500,239.547928886725 -650,16,80,30500,39000,109.02338779317503 -651,16,80,30500,39500,50.888746196140914 -652,16,80,30500,40000,42.73606982375976 -653,16,80,30500,40500,65.75935122724029 -654,16,80,30500,41000,106.68884313872147 -655,16,80,30500,41500,156.54100549486617 -656,16,80,31000,38000,561.7385153195615 -657,16,80,31000,38500,335.5692026144635 -658,16,80,31000,39000,188.0383015831574 -659,16,80,31000,39500,118.2318539104416 -660,16,80,31000,40000,100.81000168801492 -661,16,80,31000,40500,114.72014539486217 -662,16,80,31000,41000,146.2992492326178 -663,16,80,31000,41500,186.8074429488408 -664,16,80,31500,38000,697.9937997454152 -665,16,80,31500,38500,466.42234442578484 -666,16,80,31500,39000,306.52125608515166 -667,16,80,31500,39500,230.54692639209762 -668,16,80,31500,40000,206.461121102699 -669,16,80,31500,40500,212.23429887269359 -670,16,80,31500,41000,234.70913795495554 -671,16,80,31500,41500,265.8143069252357 -672,16,120,29000,38000,1085.688903883652 -673,16,120,29000,38500,750.2887000017752 -674,16,120,29000,39000,469.92662852990964 -675,16,120,29000,39500,267.1560282754928 -676,16,120,29000,40000,146.06299930062625 -677,16,120,29000,40500,95.28836772053619 -678,16,120,29000,41000,97.41466545178946 -679,16,120,29000,41500,135.3804131941845 -680,16,120,29500,38000,1079.5576154477903 -681,16,120,29500,38500,751.2932384998761 -682,16,120,29500,39000,458.27083477307207 -683,16,120,29500,39500,240.9658024131812 -684,16,120,29500,40000,109.3801465044384 -685,16,120,29500,40500,51.274139057659724 -686,16,120,29500,41000,47.36446629605638 -687,16,120,29500,41500,79.42944320845996 -688,16,120,30000,38000,1139.3792936518537 -689,16,120,30000,38500,833.7979589668842 -690,16,120,30000,39000,507.805443202025 -691,16,120,30000,39500,259.93892964607977 -692,16,120,30000,40000,108.7341499557062 -693,16,120,30000,40500,38.152937143498605 -694,16,120,30000,41000,25.403985123518716 -695,16,120,30000,41500,49.72822589160786 -696,16,120,30500,38000,1285.0396277304772 -697,16,120,30500,38500,1025.254169031627 -698,16,120,30500,39000,622.5890550779666 -699,16,120,30500,39500,333.3353043756717 -700,16,120,30500,40000,155.70268128051293 -701,16,120,30500,40500,66.84125446522368 -702,16,120,30500,41000,42.25187049753978 -703,16,120,30500,41500,56.98314898830595 -704,16,120,31000,38000,1595.7993459811262 -705,16,120,31000,38500,1252.8886556470425 -706,16,120,31000,39000,731.4408383874198 -707,16,120,31000,39500,451.0090473423308 -708,16,120,31000,40000,251.5086563526081 -709,16,120,31000,40500,141.8915050063955 -710,16,120,31000,41000,104.67474675582574 -711,16,120,31000,41500,109.1609567535697 -712,16,120,31500,38000,1942.3896021770768 -713,16,120,31500,38500,1197.207050908449 -714,16,120,31500,39000,812.6818768064074 -715,16,120,31500,39500,611.45532452889 -716,16,120,31500,40000,380.63642711770643 -717,16,120,31500,40500,258.5514125337487 -718,16,120,31500,41000,213.48518421250665 -719,16,120,31500,41500,209.58134396574906 -720,19,40,29000,38000,169.3907733115706 -721,19,40,29000,38500,212.23331960093145 -722,19,40,29000,39000,275.9376503672959 -723,19,40,29000,39500,350.4301397081139 -724,19,40,29000,40000,428.40863665493924 -725,19,40,29000,40500,504.955113902399 -726,19,40,29000,41000,577.023450987656 -727,19,40,29000,41500,642.9410032211753 -728,19,40,29500,38000,102.40889356493292 -729,19,40,29500,38500,141.19036226103668 -730,19,40,29500,39000,200.19333708701748 -731,19,40,29500,39500,269.6750686488757 -732,19,40,29500,40000,342.6217886299377 -733,19,40,29500,40500,414.33044375626207 -734,19,40,29500,41000,481.89521316730713 -735,19,40,29500,41500,543.7211700546151 -736,19,40,30000,38000,51.95330426445395 -737,19,40,30000,38500,85.69656829127965 -738,19,40,30000,39000,138.98376466247876 -739,19,40,30000,39500,202.43251598105033 -740,19,40,30000,40000,269.3557903452929 -741,19,40,30000,40500,335.2960133312316 -742,19,40,30000,41000,397.50658847538665 -743,19,40,30000,41500,454.47903112410967 -744,19,40,30500,38000,28.864802790801026 -745,19,40,30500,38500,56.32899754732796 -746,19,40,30500,39000,102.69825523352162 -747,19,40,30500,39500,158.95118263535466 -748,19,40,30500,40000,218.75241957992617 -749,19,40,30500,40500,277.9122290233915 -750,19,40,30500,41000,333.8561815041273 -751,19,40,30500,41500,385.1662652901447 -752,19,40,31000,38000,43.72359701781447 -753,19,40,31000,38500,63.683967347844224 -754,19,40,31000,39000,101.95579433282329 -755,19,40,31000,39500,149.8826019475827 -756,19,40,31000,40000,201.50605279789198 -757,19,40,31000,40500,252.92391570754876 -758,19,40,31000,41000,301.7431453727685 -759,19,40,31000,41500,346.6368192781496 -760,19,40,31500,38000,104.05710998615942 -761,19,40,31500,38500,115.95783594434451 -762,19,40,31500,39000,145.42181873662554 -763,19,40,31500,39500,184.26373455825217 -764,19,40,31500,40000,226.97066340897095 -765,19,40,31500,40500,269.96403356902357 -766,19,40,31500,41000,311.04753558871505 -767,19,40,31500,41500,348.98866332680115 -768,19,80,29000,38000,453.1314944429312 -769,19,80,29000,38500,281.24067760117225 -770,19,80,29000,39000,185.83730378881882 -771,19,80,29000,39500,154.25726305915472 -772,19,80,29000,40000,170.2912737797755 -773,19,80,29000,40500,218.38979299191152 -774,19,80,29000,41000,285.604024444273 -775,19,80,29000,41500,362.0858325427657 -776,19,80,29500,38000,400.06299682217264 -777,19,80,29500,38500,224.41725666435008 -778,19,80,29500,39000,125.58476107530382 -779,19,80,29500,39500,90.55733834394478 -780,19,80,29500,40000,102.67519971027264 -781,19,80,29500,40500,146.27807815967392 -782,19,80,29500,41000,208.57372904155937 -783,19,80,29500,41500,279.9669583078214 -784,19,80,30000,38000,376.1594584816549 -785,19,80,30000,38500,191.30452808298463 -786,19,80,30000,39000,85.63116084217559 -787,19,80,30000,39500,45.10487847849711 -788,19,80,30000,40000,51.88389644342952 -789,19,80,30000,40500,89.78942817703852 -790,19,80,30000,41000,146.0393555385696 -791,19,80,30000,41500,211.26567367707352 -792,19,80,30500,38000,401.874315275947 -793,19,80,30500,38500,197.55305366608133 -794,19,80,30500,39000,79.00348967857379 -795,19,80,30500,39500,29.602719961568614 -796,19,80,30500,40000,28.980451378502487 -797,19,80,30500,40500,59.63541802023186 -798,19,80,30500,41000,108.48607655362268 -799,19,80,30500,41500,166.30589286399507 -800,19,80,31000,38000,484.930958445979 -801,19,80,31000,38500,254.27552635537404 -802,19,80,31000,39000,116.75543721560439 -803,19,80,31000,39500,54.77547840250418 -804,19,80,31000,40000,44.637472658824976 -805,19,80,31000,40500,66.50466903927668 -806,19,80,31000,41000,106.62737262508298 -807,19,80,31000,41500,155.8310688191254 -808,19,80,31500,38000,595.6094306603337 -809,19,80,31500,38500,359.60040819463063 -810,19,80,31500,39000,201.85328967228585 -811,19,80,31500,39500,126.24442464793601 -812,19,80,31500,40000,106.07388975142673 -813,19,80,31500,40500,118.52358345403363 -814,19,80,31500,41000,149.1597537162607 -815,19,80,31500,41500,188.94964975523197 -816,19,120,29000,38000,1133.9213841599772 -817,19,120,29000,38500,793.9759807804692 -818,19,120,29000,39000,516.5580425563733 -819,19,120,29000,39500,318.60172051726147 -820,19,120,29000,40000,201.662212274693 -821,19,120,29000,40500,154.47522945829064 -822,19,120,29000,41000,160.28049502033574 -823,19,120,29000,41500,202.35345983501588 -824,19,120,29500,38000,1091.6343400395158 -825,19,120,29500,38500,754.9332443184217 -826,19,120,29500,39000,472.1777992591152 -827,19,120,29500,39500,267.03951846894995 -828,19,120,29500,40000,144.25558152688114 -829,19,120,29500,40500,92.40384156679512 -830,19,120,29500,41000,93.81833253459942 -831,19,120,29500,41500,131.24753560710644 -832,19,120,30000,38000,1092.719296892266 -833,19,120,30000,38500,764.7065490850255 -834,19,120,30000,39000,467.2268758064373 -835,19,120,30000,39500,244.9367732985332 -836,19,120,30000,40000,110.00996333393202 -837,19,120,30000,40500,49.96381544207811 -838,19,120,30000,41000,44.9298739569088 -839,19,120,30000,41500,76.25447129089613 -840,19,120,30500,38000,1160.6160120981158 -841,19,120,30500,38500,865.5953188304933 -842,19,120,30500,39000,531.1657093741892 -843,19,120,30500,39500,271.98520008106277 -844,19,120,30500,40000,114.03616090967407 -845,19,120,30500,40500,39.74252227099571 -846,19,120,30500,41000,25.07176465285551 -847,19,120,30500,41500,48.298794094852724 -848,19,120,31000,38000,1304.8870694342509 -849,19,120,31000,38500,1089.6854636757826 -850,19,120,31000,39000,668.6632735260521 -851,19,120,31000,39500,356.7751012890747 -852,19,120,31000,40000,168.32491564142487 -853,19,120,31000,40500,72.82648063377391 -854,19,120,31000,41000,45.02326687759286 -855,19,120,31000,41500,58.13111530831655 -856,19,120,31500,38000,1645.2697164013964 -857,19,120,31500,38500,1373.859712069864 -858,19,120,31500,39000,787.3948673670299 -859,19,120,31500,39500,483.60546305948367 -860,19,120,31500,40000,273.4285373433001 -861,19,120,31500,40500,153.21079535396908 -862,19,120,31500,41000,111.21299419905313 -863,19,120,31500,41500,113.52006337929113 -864,22,40,29000,38000,229.2032513971666 -865,22,40,29000,38500,274.65023153674116 -866,22,40,29000,39000,341.4424739822062 -867,22,40,29000,39500,419.2624324130753 -868,22,40,29000,40000,500.6022690006133 -869,22,40,29000,40500,580.3923016374031 -870,22,40,29000,41000,655.4874207991389 -871,22,40,29000,41500,724.1595537770351 -872,22,40,29500,38000,155.45206306046595 -873,22,40,29500,38500,197.41588482427002 -874,22,40,29500,39000,260.1641484982308 -875,22,40,29500,39500,333.666918810689 -876,22,40,29500,40000,410.66541588422854 -877,22,40,29500,40500,486.276072112155 -878,22,40,29500,41000,557.4760464927683 -879,22,40,29500,41500,622.6057687448293 -880,22,40,30000,38000,90.70026588811803 -881,22,40,30000,38500,128.41239603755494 -882,22,40,30000,39000,186.27261386900233 -883,22,40,30000,39500,254.5802373859711 -884,22,40,30000,40000,326.3686182341553 -885,22,40,30000,40500,396.9735001502319 -886,22,40,30000,41000,463.5155278718613 -887,22,40,30000,41500,524.414569320113 -888,22,40,30500,38000,44.551475763397946 -889,22,40,30500,38500,76.95264448905411 -890,22,40,30500,39000,128.85898727872572 -891,22,40,30500,39500,190.91422001003792 -892,22,40,30500,40000,256.4755613806196 -893,22,40,30500,40500,321.125224208803 -894,22,40,30500,41000,382.14434919800453 -895,22,40,30500,41500,438.03974322333033 -896,22,40,31000,38000,28.101321546315717 -897,22,40,31000,38500,53.867829756398805 -898,22,40,31000,39000,98.57619184859544 -899,22,40,31000,39500,153.19473192134507 -900,22,40,31000,40000,211.4202434313414 -901,22,40,31000,40500,269.09905982026265 -902,22,40,31000,41000,323.68306330754416 -903,22,40,31000,41500,373.76836451736045 -904,22,40,31500,38000,51.648288279447364 -905,22,40,31500,38500,69.56074881661863 -906,22,40,31500,39000,105.91402675097291 -907,22,40,31500,39500,151.99456204656389 -908,22,40,31500,40000,201.85995274525234 -909,22,40,31500,40500,251.63807959916412 -910,22,40,31500,41000,298.9593498669657 -911,22,40,31500,41500,342.50888994628025 -912,22,80,29000,38000,507.5440336860194 -913,22,80,29000,38500,336.42019672232965 -914,22,80,29000,39000,242.21016116765423 -915,22,80,29000,39500,212.33396533224905 -916,22,80,29000,40000,230.67632355958136 -917,22,80,29000,40500,281.6224662955561 -918,22,80,29000,41000,352.0457411487133 -919,22,80,29000,41500,431.89288175778637 -920,22,80,29500,38000,443.2889283037078 -921,22,80,29500,38500,270.0648237630224 -922,22,80,29500,39000,173.57666711629645 -923,22,80,29500,39500,141.06258420240613 -924,22,80,29500,40000,156.18412870159142 -925,22,80,29500,40500,203.33105261575707 -926,22,80,29500,41000,269.5552387411201 -927,22,80,29500,41500,345.03801326123767 -928,22,80,30000,38000,395.34177505602497 -929,22,80,30000,38500,217.11094192826982 -930,22,80,30000,39000,116.38535634181476 -931,22,80,30000,39500,79.94742924888467 -932,22,80,30000,40000,90.84706550421288 -933,22,80,30000,40500,133.26308067939766 -934,22,80,30000,41000,194.36064414396228 -935,22,80,30000,41500,264.56059537656466 -936,22,80,30500,38000,382.0341866812038 -937,22,80,30500,38500,191.65621311671836 -938,22,80,30500,39000,82.3318677587146 -939,22,80,30500,39500,39.44606931321677 -940,22,80,30500,40000,44.476166488763134 -941,22,80,30500,40500,80.84561981845566 -942,22,80,30500,41000,135.62459431793735 -943,22,80,30500,41500,199.42208168600175 -944,22,80,31000,38000,425.5181957619983 -945,22,80,31000,38500,210.2667219741389 -946,22,80,31000,39000,84.97041062888985 -947,22,80,31000,39500,31.593073529038755 -948,22,80,31000,40000,28.407154164211214 -949,22,80,31000,40500,57.05446633976857 -950,22,80,31000,41000,104.10423883907688 -951,22,80,31000,41500,160.23135976433713 -952,22,80,31500,38000,527.5015417150911 -953,22,80,31500,38500,282.29650611769665 -954,22,80,31500,39000,134.62881845323489 -955,22,80,31500,39500,66.62736532046851 -956,22,80,31500,40000,52.9918858786988 -957,22,80,31500,40500,72.36913743145999 -958,22,80,31500,41000,110.38003828747726 -959,22,80,31500,41500,157.65470091455973 -960,22,120,29000,38000,1186.823326813257 -961,22,120,29000,38500,844.3317816964005 -962,22,120,29000,39000,567.7367986440256 -963,22,120,29000,39500,371.79782508970567 -964,22,120,29000,40000,256.9261857702517 -965,22,120,29000,40500,211.85466060592006 -966,22,120,29000,41000,220.09534855737033 -967,22,120,29000,41500,265.02731793490034 -968,22,120,29500,38000,1128.4568915685559 -969,22,120,29500,38500,787.7709648712951 -970,22,120,29500,39000,508.4832626962424 -971,22,120,29500,39500,308.52654841064975 -972,22,120,29500,40000,190.01030358402707 -973,22,120,29500,40500,141.62663282114926 -974,22,120,29500,41000,146.40704203984612 -975,22,120,29500,41500,187.48734389188584 -976,22,120,30000,38000,1094.7007205604846 -977,22,120,30000,38500,757.7313528729464 -978,22,120,30000,39000,471.282561364766 -979,22,120,30000,39500,262.0412520036699 -980,22,120,30000,40000,136.26956239282435 -981,22,120,30000,40500,82.4268827471484 -982,22,120,30000,41000,82.3695177584498 -983,22,120,30000,41500,118.51210034475737 -984,22,120,30500,38000,1111.0872182758205 -985,22,120,30500,38500,787.2204655558988 -986,22,120,30500,39000,481.85960605002055 -987,22,120,30500,39500,250.28740868446397 -988,22,120,30500,40000,109.21968920710272 -989,22,120,30500,40500,45.51600269221681 -990,22,120,30500,41000,38.172157811051115 -991,22,120,30500,41500,67.73748641348168 -992,22,120,31000,38000,1193.3958874354898 -993,22,120,31000,38500,923.0731791194576 -994,22,120,31000,39000,573.4457650536078 -995,22,120,31000,39500,294.2980811757103 -996,22,120,31000,40000,124.86249624679849 -997,22,120,31000,40500,43.948524347749846 -998,22,120,31000,41000,25.582084045731808 -999,22,120,31000,41500,46.36268252714472 -1000,22,120,31500,38000,1336.0993444856913 -1001,22,120,31500,38500,1194.893001664831 -1002,22,120,31500,39000,740.6584250286721 -1003,22,120,31500,39500,397.18127104230757 -1004,22,120,31500,40000,194.20390582893873 -1005,22,120,31500,40500,88.22588964369922 -1006,22,120,31500,41000,54.97797247760634 -1007,22,120,31500,41500,64.88195101638016 diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py deleted file mode 100644 index ff1287811cf..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py +++ /dev/null @@ -1,57 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -The following script can be used to run semibatch parameter estimation in -parallel and save results to files for later analysis and graphics. -Example command: mpiexec -n 4 python parallel_example.py -""" -import numpy as np -import pandas as pd -from itertools import product -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model - - -def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] - - # Data, list of json file names - data = [] - file_dirname = dirname(abspath(str(__file__))) - for exp_num in range(10): - file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) - data.append(file_name) - - # Note, the model already includes a 'SecondStageCost' expression - # for sum of squared error that will be used in parameter estimation - - pest = parmest.Estimator(generate_model, data, theta_names) - - ### Parameter estimation with bootstrap resampling - bootstrap_theta = pest.theta_est_bootstrap(100) - bootstrap_theta.to_csv('bootstrap_theta.csv') - - ### Compute objective at theta for likelihood ratio test - k1 = np.arange(4, 24, 3) - k2 = np.arange(40, 160, 40) - E1 = np.arange(29000, 32000, 500) - E2 = np.arange(38000, 42000, 500) - theta_vals = pd.DataFrame(list(product(k1, k2, E1, E2)), columns=theta_names) - - obj_at_theta = pest.objective_at_theta(theta_vals) - obj_at_theta.to_csv('obj_at_theta.csv') - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py deleted file mode 100644 index fc4c9f5c675..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py +++ /dev/null @@ -1,42 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import json -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model - - -def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] - - # Data, list of dictionaries - data = [] - file_dirname = dirname(abspath(str(__file__))) - for exp_num in range(10): - file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) - with open(file_name, 'r') as infile: - d = json.load(infile) - data.append(d) - - # Note, the model already includes a 'SecondStageCost' expression - # for sum of squared error that will be used in parameter estimation - - pest = parmest.Estimator(generate_model, data, theta_names) - - obj, theta = pest.theta_est() - print(obj) - print(theta) - - -if __name__ == '__main__': - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py deleted file mode 100644 index 071e53236c4..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py +++ /dev/null @@ -1,52 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import json -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model -import pyomo.contrib.parmest.scenariocreator as sc - - -def main(): - # Vars to estimate in parmest - theta_names = ['k1', 'k2', 'E1', 'E2'] - - # Data: list of dictionaries - data = [] - file_dirname = dirname(abspath(str(__file__))) - for exp_num in range(10): - fname = join(file_dirname, 'exp' + str(exp_num + 1) + '.out') - with open(fname, 'r') as infile: - d = json.load(infile) - data.append(d) - - pest = parmest.Estimator(generate_model, data, theta_names) - - scenmaker = sc.ScenarioCreator(pest, "ipopt") - - # Make one scenario per experiment and write to a csv file - output_file = "scenarios.csv" - experimentscens = sc.ScenarioSet("Experiments") - scenmaker.ScenariosFromExperiments(experimentscens) - experimentscens.write_csv(output_file) - - # Use the bootstrap to make 3 scenarios and print - bootscens = sc.ScenarioSet("Bootstrap") - scenmaker.ScenariosFromBootstrap(bootscens, 3) - for s in bootscens.ScensIterator(): - print("{}, {}".format(s.name, s.probability)) - for n, v in s.ThetaVals.items(): - print(" {}={}".format(n, v)) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv deleted file mode 100644 index 22f9a651bc3..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv +++ /dev/null @@ -1,11 +0,0 @@ -Name,Probability,k1,k2,E1,E2 -ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 -ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 -ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 -ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 -ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 -ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 -ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 -ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py deleted file mode 100644 index 6762531a338..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py +++ /dev/null @@ -1,287 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -""" -Semibatch model, based on Nicholson et al. (2018). pyomo.dae: A modeling and -automatic discretization framework for optimization with di -erential and -algebraic equations. Mathematical Programming Computation, 10(2), 187-223. -""" -import json -from os.path import join, abspath, dirname -from pyomo.environ import ( - ConcreteModel, - Set, - Param, - Var, - Constraint, - ConstraintList, - Expression, - Objective, - TransformationFactory, - SolverFactory, - exp, - minimize, -) -from pyomo.dae import ContinuousSet, DerivativeVar - - -def generate_model(data): - # if data is a file name, then load file first - if isinstance(data, str): - file_name = data - try: - with open(file_name, "r") as infile: - data = json.load(infile) - except: - raise RuntimeError(f"Could not read {file_name} as json") - - # unpack and fix the data - cameastemp = data["Ca_meas"] - cbmeastemp = data["Cb_meas"] - ccmeastemp = data["Cc_meas"] - trmeastemp = data["Tr_meas"] - - cameas = {} - cbmeas = {} - ccmeas = {} - trmeas = {} - for i in cameastemp.keys(): - cameas[float(i)] = cameastemp[i] - cbmeas[float(i)] = cbmeastemp[i] - ccmeas[float(i)] = ccmeastemp[i] - trmeas[float(i)] = trmeastemp[i] - - m = ConcreteModel() - - # - # Measurement Data - # - m.measT = Set(initialize=sorted(cameas.keys())) - m.Ca_meas = Param(m.measT, initialize=cameas) - m.Cb_meas = Param(m.measT, initialize=cbmeas) - m.Cc_meas = Param(m.measT, initialize=ccmeas) - m.Tr_meas = Param(m.measT, initialize=trmeas) - - # - # Parameters for semi-batch reactor model - # - m.R = Param(initialize=8.314) # kJ/kmol/K - m.Mwa = Param(initialize=50.0) # kg/kmol - m.rhor = Param(initialize=1000.0) # kg/m^3 - m.cpr = Param(initialize=3.9) # kJ/kg/K - m.Tf = Param(initialize=300) # K - m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol - m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol - m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K - m.alphac = Param(initialize=0.7) # kJ/s/m^2/K - m.Aj = Param(initialize=5.0) # m^2 - m.Ac = Param(initialize=3.0) # m^2 - m.Vj = Param(initialize=0.9) # m^3 - m.Vc = Param(initialize=0.07) # m^3 - m.rhow = Param(initialize=700.0) # kg/m^3 - m.cpw = Param(initialize=3.1) # kJ/kg/K - m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) - m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) - m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) - m.Tr0 = Param(initialize=300.0) # K - m.Vr0 = Param(initialize=1.0) # m^3 - - m.time = ContinuousSet(bounds=(0, 21600), initialize=m.measT) # Time in seconds - - # - # Control Inputs - # - def _initTc(m, t): - if t < 10800: - return data["Tc1"] - else: - return data["Tc2"] - - m.Tc = Param( - m.time, initialize=_initTc, default=_initTc - ) # bounds= (288,432) Cooling coil temp, control input - - def _initFa(m, t): - if t < 10800: - return data["Fa1"] - else: - return data["Fa2"] - - m.Fa = Param( - m.time, initialize=_initFa, default=_initFa - ) # bounds=(0,0.05) Inlet flow rate, control input - - # - # Parameters being estimated - # - m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 - m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 - m.E1 = Var(initialize=27000.0, bounds=(25000, 40000)) # kJ/kmol Actual: 30000 - m.E2 = Var(initialize=45000.0, bounds=(35000, 50000)) # kJ/kmol Actual: 40000 - # m.E1.fix(30000) - # m.E2.fix(40000) - - # - # Time dependent variables - # - m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) - m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) - m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) - m.Vr = Var(m.time, initialize=m.Vr0) - m.Tr = Var(m.time, initialize=m.Tr0) - m.Tj = Var( - m.time, initialize=310.0, bounds=(288, None) - ) # Cooling jacket temp, follows coil temp until failure - - # - # Derivatives in the model - # - m.dCa = DerivativeVar(m.Ca) - m.dCb = DerivativeVar(m.Cb) - m.dCc = DerivativeVar(m.Cc) - m.dVr = DerivativeVar(m.Vr) - m.dTr = DerivativeVar(m.Tr) - - # - # Differential Equations in the model - # - - def _dCacon(m, t): - if t == 0: - return Constraint.Skip - return ( - m.dCa[t] - == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] - ) - - m.dCacon = Constraint(m.time, rule=_dCacon) - - def _dCbcon(m, t): - if t == 0: - return Constraint.Skip - return ( - m.dCb[t] - == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] - - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] - ) - - m.dCbcon = Constraint(m.time, rule=_dCbcon) - - def _dCccon(m, t): - if t == 0: - return Constraint.Skip - return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] - - m.dCccon = Constraint(m.time, rule=_dCccon) - - def _dVrcon(m, t): - if t == 0: - return Constraint.Skip - return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor - - m.dVrcon = Constraint(m.time, rule=_dVrcon) - - def _dTrcon(m, t): - if t == 0: - return Constraint.Skip - return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[t] * ( - m.Tf - m.Tr[t] - ) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] * m.deltaH1 - m.k2 * exp( - -m.E2 / (m.R * m.Tr[t]) - ) * m.Cb[ - t - ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( - m.Tj[t] - m.Tr[t] - ) + m.alphac * m.Ac / m.Vr0 * ( - m.Tc[t] - m.Tr[t] - ) - - m.dTrcon = Constraint(m.time, rule=_dTrcon) - - def _singlecooling(m, t): - return m.Tc[t] == m.Tj[t] - - m.singlecooling = Constraint(m.time, rule=_singlecooling) - - # Initial Conditions - def _initcon(m): - yield m.Ca[m.time.first()] == m.Ca0 - yield m.Cb[m.time.first()] == m.Cb0 - yield m.Cc[m.time.first()] == m.Cc0 - yield m.Vr[m.time.first()] == m.Vr0 - yield m.Tr[m.time.first()] == m.Tr0 - - m.initcon = ConstraintList(rule=_initcon) - - # - # Stage-specific cost computations - # - def ComputeFirstStageCost_rule(model): - return 0 - - m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) - - def AllMeasurements(m): - return sum( - (m.Ca[t] - m.Ca_meas[t]) ** 2 - + (m.Cb[t] - m.Cb_meas[t]) ** 2 - + (m.Cc[t] - m.Cc_meas[t]) ** 2 - + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 - for t in m.measT - ) - - def MissingMeasurements(m): - if data["experiment"] == 1: - return sum( - (m.Ca[t] - m.Ca_meas[t]) ** 2 - + (m.Cb[t] - m.Cb_meas[t]) ** 2 - + (m.Cc[t] - m.Cc_meas[t]) ** 2 - + (m.Tr[t] - m.Tr_meas[t]) ** 2 - for t in m.measT - ) - elif data["experiment"] == 2: - return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) - else: - return sum( - (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 - for t in m.measT - ) - - m.SecondStageCost = Expression(rule=MissingMeasurements) - - def total_cost_rule(model): - return model.FirstStageCost + model.SecondStageCost - - m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) - - # Discretize model - disc = TransformationFactory("dae.collocation") - disc.apply_to(m, nfe=20, ncp=4) - return m - - -def main(): - # Data loaded from files - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "exp2.out")) - with open(file_name, "r") as infile: - data = json.load(infile) - data["experiment"] = 2 - - model = generate_model(data) - solver = SolverFactory("ipopt") - solver.solve(model) - print("k1 = ", model.k1()) - print("E1 = ", model.E1()) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/parmest.py b/pyomo/contrib/parmest/deprecated/parmest.py deleted file mode 100644 index 82bf893dd06..00000000000 --- a/pyomo/contrib/parmest/deprecated/parmest.py +++ /dev/null @@ -1,1361 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -#### Using mpi-sppy instead of PySP; May 2020 -#### Adding option for "local" EF starting Sept 2020 -#### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 - -# TODO: move use_mpisppy to a Pyomo configuration option -# -# False implies always use the EF that is local to parmest -use_mpisppy = True # Use it if we can but use local if not. -if use_mpisppy: - try: - # MPI-SPPY has an unfortunate side effect of outputting - # "[ 0.00] Initializing mpi-sppy" when it is imported. This can - # cause things like doctests to fail. We will suppress that - # information here. - from pyomo.common.tee import capture_output - - with capture_output(): - import mpisppy.utils.sputils as sputils - except ImportError: - use_mpisppy = False # we can't use it -if use_mpisppy: - # These things should be outside the try block. - sputils.disable_tictoc_output() - import mpisppy.opt.ef as st - import mpisppy.scenario_tree as scenario_tree -else: - import pyomo.contrib.parmest.utils.create_ef as local_ef - import pyomo.contrib.parmest.utils.scenario_tree as scenario_tree - -import re -import importlib as im -import logging -import types -import json -from itertools import combinations - -from pyomo.common.dependencies import ( - attempt_import, - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, -) - -import pyomo.environ as pyo - -from pyomo.opt import SolverFactory -from pyomo.environ import Block, ComponentUID - -import pyomo.contrib.parmest.utils as utils -import pyomo.contrib.parmest.graphics as graphics -from pyomo.dae import ContinuousSet - -parmest_available = numpy_available & pandas_available & scipy_available - -inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( - 'pyomo.contrib.interior_point.inverse_reduced_hessian' -) - -logger = logging.getLogger(__name__) - - -def ef_nonants(ef): - # Wrapper to call someone's ef_nonants - # (the function being called is very short, but it might be changed) - if use_mpisppy: - return sputils.ef_nonants(ef) - else: - return local_ef.ef_nonants(ef) - - -def _experiment_instance_creation_callback( - scenario_name, node_names=None, cb_data=None -): - """ - This is going to be called by mpi-sppy or the local EF and it will call into - the user's model's callback. - - Parameters: - ----------- - scenario_name: `str` Scenario name should end with a number - node_names: `None` ( Not used here ) - cb_data : dict with ["callback"], ["BootList"], - ["theta_names"], ["cb_data"], etc. - "cb_data" is passed through to user's callback function - that is the "callback" value. - "BootList" is None or bootstrap experiment number list. - (called cb_data by mpisppy) - - - Returns: - -------- - instance: `ConcreteModel` - instantiated scenario - - Note: - ---- - There is flexibility both in how the function is passed and its signature. - """ - assert cb_data is not None - outer_cb_data = cb_data - scen_num_str = re.compile(r'(\d+)$').search(scenario_name).group(1) - scen_num = int(scen_num_str) - basename = scenario_name[: -len(scen_num_str)] # to reconstruct name - - CallbackFunction = outer_cb_data["callback"] - - if callable(CallbackFunction): - callback = CallbackFunction - else: - cb_name = CallbackFunction - - if "CallbackModule" not in outer_cb_data: - raise RuntimeError( - "Internal Error: need CallbackModule in parmest callback" - ) - else: - modname = outer_cb_data["CallbackModule"] - - if isinstance(modname, str): - cb_module = im.import_module(modname, package=None) - elif isinstance(modname, types.ModuleType): - cb_module = modname - else: - print("Internal Error: bad CallbackModule") - raise - - try: - callback = getattr(cb_module, cb_name) - except: - print("Error getting function=" + cb_name + " from module=" + str(modname)) - raise - - if "BootList" in outer_cb_data: - bootlist = outer_cb_data["BootList"] - # print("debug in callback: using bootlist=",str(bootlist)) - # assuming bootlist itself is zero based - exp_num = bootlist[scen_num] - else: - exp_num = scen_num - - scen_name = basename + str(exp_num) - - cb_data = outer_cb_data["cb_data"] # cb_data might be None. - - # at least three signatures are supported. The first is preferred - try: - instance = callback(experiment_number=exp_num, cb_data=cb_data) - except TypeError: - raise RuntimeError( - "Only one callback signature is supported: " - "callback(experiment_number, cb_data) " - ) - """ - try: - instance = callback(scenario_tree_model, scen_name, node_names) - except TypeError: # deprecated signature? - try: - instance = callback(scen_name, node_names) - except: - print("Failed to create instance using callback; TypeError+") - raise - except: - print("Failed to create instance using callback.") - raise - """ - if hasattr(instance, "_mpisppy_node_list"): - raise RuntimeError(f"scenario for experiment {exp_num} has _mpisppy_node_list") - nonant_list = [ - instance.find_component(vstr) for vstr in outer_cb_data["theta_names"] - ] - if use_mpisppy: - instance._mpisppy_node_list = [ - scenario_tree.ScenarioNode( - name="ROOT", - cond_prob=1.0, - stage=1, - cost_expression=instance.FirstStageCost, - nonant_list=nonant_list, - scen_model=instance, - ) - ] - else: - instance._mpisppy_node_list = [ - scenario_tree.ScenarioNode( - name="ROOT", - cond_prob=1.0, - stage=1, - cost_expression=instance.FirstStageCost, - scen_name_list=None, - nonant_list=nonant_list, - scen_model=instance, - ) - ] - - if "ThetaVals" in outer_cb_data: - thetavals = outer_cb_data["ThetaVals"] - - # dlw august 2018: see mea code for more general theta - for vstr in thetavals: - theta_cuid = ComponentUID(vstr) - theta_object = theta_cuid.find_component_on(instance) - if thetavals[vstr] is not None: - # print("Fixing",vstr,"at",str(thetavals[vstr])) - theta_object.fix(thetavals[vstr]) - else: - # print("Freeing",vstr) - theta_object.unfix() - - return instance - - -# ============================================= -def _treemaker(scenlist): - """ - Makes a scenario tree (avoids dependence on daps) - - Parameters - ---------- - scenlist (list of `int`): experiment (i.e. scenario) numbers - - Returns - ------- - a `ConcreteModel` that is the scenario tree - """ - - num_scenarios = len(scenlist) - m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() - m = m.create_instance() - m.Stages.add('Stage1') - m.Stages.add('Stage2') - m.Nodes.add('RootNode') - for i in scenlist: - m.Nodes.add('LeafNode_Experiment' + str(i)) - m.Scenarios.add('Experiment' + str(i)) - m.NodeStage['RootNode'] = 'Stage1' - m.ConditionalProbability['RootNode'] = 1.0 - for node in m.Nodes: - if node != 'RootNode': - m.NodeStage[node] = 'Stage2' - m.Children['RootNode'].add(node) - m.Children[node].clear() - m.ConditionalProbability[node] = 1.0 / num_scenarios - m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - - return m - - -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - - -class _SecondStageCostExpr(object): - """ - Class to pass objective expression into the Pyomo model - """ - - def __init__(self, ssc_function, data): - self._ssc_function = ssc_function - self._data = data - - def __call__(self, model): - return self._ssc_function(model, self._data) - - -class Estimator(object): - """ - Parameter estimation class - - Parameters - ---------- - model_function: function - Function that generates an instance of the Pyomo model using 'data' - as the input argument - data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names - Data that is used to build an instance of the Pyomo model and build - the objective function - theta_names: list of strings - List of Var names to estimate - obj_function: function, optional - Function used to formulate parameter estimation objective, generally - sum of squared error between measurements and model variables. - If no function is specified, the model is used - "as is" and should be defined with a "FirstStageCost" and - "SecondStageCost" expression that are used to build an objective. - tee: bool, optional - Indicates that ef solver output should be teed - diagnostic_mode: bool, optional - If True, print diagnostics from the solver - solver_options: dict, optional - Provides options to the solver (also the name of an attribute) - """ - - def __init__( - self, - model_function, - data, - theta_names, - obj_function=None, - tee=False, - diagnostic_mode=False, - solver_options=None, - ): - self.model_function = model_function - - assert isinstance( - data, (list, pd.DataFrame) - ), "Data must be a list or DataFrame" - # convert dataframe into a list of dataframes, each row = one scenario - if isinstance(data, pd.DataFrame): - self.callback_data = [ - data.loc[i, :].to_frame().transpose() for i in data.index - ] - else: - self.callback_data = data - assert isinstance( - self.callback_data[0], (dict, pd.DataFrame, str) - ), "The scenarios in data must be a dictionary, DataFrame or filename" - - if len(theta_names) == 0: - self.theta_names = ['parmest_dummy_var'] - else: - self.theta_names = theta_names - - self.obj_function = obj_function - self.tee = tee - self.diagnostic_mode = diagnostic_mode - self.solver_options = solver_options - - self._second_stage_cost_exp = "SecondStageCost" - # boolean to indicate if model is initialized using a square solve - self.model_initialized = False - - def _return_theta_names(self): - """ - Return list of fitted model parameter names - """ - # if fitted model parameter names differ from theta_names created when Estimator object is created - if hasattr(self, 'theta_names_updated'): - return self.theta_names_updated - - else: - return ( - self.theta_names - ) # default theta_names, created when Estimator object is created - - def _create_parmest_model(self, data): - """ - Modify the Pyomo model for parameter estimation - """ - model = self.model_function(data) - - if (len(self.theta_names) == 1) and ( - self.theta_names[0] == 'parmest_dummy_var' - ): - model.parmest_dummy_var = pyo.Var(initialize=1.0) - - # Add objective function (optional) - if self.obj_function: - for obj in model.component_objects(pyo.Objective): - if obj.name in ["Total_Cost_Objective"]: - raise RuntimeError( - "Parmest will not override the existing model Objective named " - + obj.name - ) - obj.deactivate() - - for expr in model.component_data_objects(pyo.Expression): - if expr.name in ["FirstStageCost", "SecondStageCost"]: - raise RuntimeError( - "Parmest will not override the existing model Expression named " - + expr.name - ) - model.FirstStageCost = pyo.Expression(expr=0) - model.SecondStageCost = pyo.Expression( - rule=_SecondStageCostExpr(self.obj_function, data) - ) - - def TotalCost_rule(model): - return model.FirstStageCost + model.SecondStageCost - - model.Total_Cost_Objective = pyo.Objective( - rule=TotalCost_rule, sense=pyo.minimize - ) - - # Convert theta Params to Vars, and unfix theta Vars - model = utils.convert_params_to_vars(model, self.theta_names) - - # Update theta names list to use CUID string representation - for i, theta in enumerate(self.theta_names): - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(model) - if var_validate is None: - logger.warning( - "theta_name[%s] (%s) was not found on the model", (i, theta) - ) - else: - try: - # If the component is not a variable, - # this will generate an exception (and the warning - # in the 'except') - var_validate.unfix() - self.theta_names[i] = repr(var_cuid) - except: - logger.warning(theta + ' is not a variable') - - self.parmest_model = model - - return model - - def _instance_creation_callback(self, experiment_number=None, cb_data=None): - # cb_data is a list of dictionaries, list of dataframes, OR list of json file names - exp_data = cb_data[experiment_number] - if isinstance(exp_data, (dict, pd.DataFrame)): - pass - elif isinstance(exp_data, str): - try: - with open(exp_data, 'r') as infile: - exp_data = json.load(infile) - except: - raise RuntimeError(f'Could not read {exp_data} as json') - else: - raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') - model = self._create_parmest_model(exp_data) - - return model - - def _Q_opt( - self, - ThetaVals=None, - solver="ef_ipopt", - return_values=[], - bootlist=None, - calc_cov=False, - cov_n=None, - ): - """ - Set up all thetas as first stage Vars, return resulting theta - values as well as the objective function value. - - """ - if solver == "k_aug": - raise RuntimeError("k_aug no longer supported.") - - # (Bootstrap scenarios will use indirection through the bootlist) - if bootlist is None: - scenario_numbers = list(range(len(self.callback_data))) - scen_names = ["Scenario{}".format(i) for i in scenario_numbers] - else: - scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] - - # tree_model.CallbackModule = None - outer_cb_data = dict() - outer_cb_data["callback"] = self._instance_creation_callback - if ThetaVals is not None: - outer_cb_data["ThetaVals"] = ThetaVals - if bootlist is not None: - outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = self.callback_data # None is OK - outer_cb_data["theta_names"] = self.theta_names - - options = {"solver": "ipopt"} - scenario_creator_options = {"cb_data": outer_cb_data} - if use_mpisppy: - ef = sputils.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - else: - ef = local_ef.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - self.ef_instance = ef - - # Solve the extensive form with ipopt - if solver == "ef_ipopt": - if not calc_cov: - # Do not calculate the reduced hessian - - solver = SolverFactory('ipopt') - if self.solver_options is not None: - for key in self.solver_options: - solver.options[key] = self.solver_options[key] - - solve_result = solver.solve(self.ef_instance, tee=self.tee) - - # The import error will be raised when we attempt to use - # inv_reduced_hessian_barrier below. - # - # elif not asl_available: - # raise ImportError("parmest requires ASL to calculate the " - # "covariance matrix with solver 'ipopt'") - else: - # parmest makes the fitted parameters stage 1 variables - ind_vars = [] - for ndname, Var, solval in ef_nonants(ef): - ind_vars.append(Var) - # calculate the reduced hessian - (solve_result, inv_red_hes) = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) - ) - - if self.diagnostic_mode: - print( - ' Solver termination condition = ', - str(solve_result.solver.termination_condition), - ) - - # assume all first stage are thetas... - thetavals = {} - for ndname, Var, solval in ef_nonants(ef): - # process the name - # the scenarios are blocks, so strip the scenario name - vname = Var.name[Var.name.find(".") + 1 :] - thetavals[vname] = solval - - objval = pyo.value(ef.EF_Obj) - - if calc_cov: - # Calculate the covariance matrix - - # Number of data points considered - n = cov_n - - # Extract number of fitted parameters - l = len(thetavals) - - # Assumption: Objective value is sum of squared errors - sse = objval - - '''Calculate covariance assuming experimental observation errors are - independent and follow a Gaussian - distribution with constant variance. - - The formula used in parmest was verified against equations (7-5-15) and - (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. - - This formula is also applicable if the objective is scaled by a constant; - the constant cancels out. (was scaled by 1/n because it computes an - expected value.) - ''' - cov = 2 * sse / (n - l) * inv_red_hes - cov = pd.DataFrame( - cov, index=thetavals.keys(), columns=thetavals.keys() - ) - - thetavals = pd.Series(thetavals) - - if len(return_values) > 0: - var_values = [] - if len(scen_names) > 1: # multiple scenarios - block_objects = self.ef_instance.component_objects( - Block, descend_into=False - ) - else: # single scenario - block_objects = [self.ef_instance] - for exp_i in block_objects: - vals = {} - for var in return_values: - exp_i_var = exp_i.find_component(str(var)) - if ( - exp_i_var is None - ): # we might have a block such as _mpisppy_data - continue - # if value to return is ContinuousSet - if type(exp_i_var) == ContinuousSet: - temp = list(exp_i_var) - else: - temp = [pyo.value(_) for _ in exp_i_var.values()] - if len(temp) == 1: - vals[var] = temp[0] - else: - vals[var] = temp - if len(vals) > 0: - var_values.append(vals) - var_values = pd.DataFrame(var_values) - if calc_cov: - return objval, thetavals, var_values, cov - else: - return objval, thetavals, var_values - - if calc_cov: - return objval, thetavals, cov - else: - return objval, thetavals - - else: - raise RuntimeError("Unknown solver in Q_Opt=" + solver) - - def _Q_at_theta(self, thetavals, initialize_parmest_model=False): - """ - Return the objective function value with fixed theta values. - - Parameters - ---------- - thetavals: dict - A dictionary of theta values. - - initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True - - Returns - ------- - objectiveval: float - The objective function value. - thetavals: dict - A dictionary of all values for theta that were input. - solvertermination: Pyomo TerminationCondition - Tries to return the "worst" solver status across the scenarios. - pyo.TerminationCondition.optimal is the best and - pyo.TerminationCondition.infeasible is the worst. - """ - - optimizer = pyo.SolverFactory('ipopt') - - if len(thetavals) > 0: - dummy_cb = { - "callback": self._instance_creation_callback, - "ThetaVals": thetavals, - "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, - } - else: - dummy_cb = { - "callback": self._instance_creation_callback, - "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, - } - - if self.diagnostic_mode: - if len(thetavals) > 0: - print(' Compute objective at theta = ', str(thetavals)) - else: - print(' Compute objective at initial theta') - - # start block of code to deal with models with no constraints - # (ipopt will crash or complain on such problems without special care) - instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) - try: # deal with special problems so Ipopt will not crash - first = next(instance.component_objects(pyo.Constraint, active=True)) - active_constraints = True - except: - active_constraints = False - # end block of code to deal with models with no constraints - - WorstStatus = pyo.TerminationCondition.optimal - totobj = 0 - scenario_numbers = list(range(len(self.callback_data))) - if initialize_parmest_model: - # create dictionary to store pyomo model instances (scenarios) - scen_dict = dict() - - for snum in scenario_numbers: - sname = "scenario_NODE" + str(snum) - instance = _experiment_instance_creation_callback(sname, None, dummy_cb) - - if initialize_parmest_model: - # list to store fitted parameter names that will be unfixed - # after initialization - theta_init_vals = [] - # use appropriate theta_names member - theta_ref = self._return_theta_names() - - for i, theta in enumerate(theta_ref): - # Use parser in ComponentUID to locate the component - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(instance) - if var_validate is None: - logger.warning( - "theta_name %s was not found on the model", (theta) - ) - else: - try: - if len(thetavals) == 0: - var_validate.fix() - else: - var_validate.fix(thetavals[theta]) - theta_init_vals.append(var_validate) - except: - logger.warning( - 'Unable to fix model parameter value for %s (not a Pyomo model Var)', - (theta), - ) - - if active_constraints: - if self.diagnostic_mode: - print(' Experiment = ', snum) - print(' First solve with special diagnostics wrapper') - (status_obj, solved, iters, time, regu) = ( - utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 - ) - ) - print( - " status_obj, solved, iters, time, regularization_stat = ", - str(status_obj), - str(solved), - str(iters), - str(time), - str(regu), - ) - - results = optimizer.solve(instance) - if self.diagnostic_mode: - print( - 'standard solve solver termination condition=', - str(results.solver.termination_condition), - ) - - if ( - results.solver.termination_condition - != pyo.TerminationCondition.optimal - ): - # DLW: Aug2018: not distinguishing "middlish" conditions - if WorstStatus != pyo.TerminationCondition.infeasible: - WorstStatus = results.solver.termination_condition - if initialize_parmest_model: - if self.diagnostic_mode: - print( - "Scenario {:d} infeasible with initialized parameter values".format( - snum - ) - ) - else: - if initialize_parmest_model: - if self.diagnostic_mode: - print( - "Scenario {:d} initialization successful with initial parameter values".format( - snum - ) - ) - if initialize_parmest_model: - # unfix parameters after initialization - for theta in theta_init_vals: - theta.unfix() - scen_dict[sname] = instance - else: - if initialize_parmest_model: - # unfix parameters after initialization - for theta in theta_init_vals: - theta.unfix() - scen_dict[sname] = instance - - objobject = getattr(instance, self._second_stage_cost_exp) - objval = pyo.value(objobject) - totobj += objval - - retval = totobj / len(scenario_numbers) # -1?? - if initialize_parmest_model and not hasattr(self, 'ef_instance'): - # create extensive form of the model using scenario dictionary - if len(scen_dict) > 0: - for scen in scen_dict.values(): - scen._mpisppy_probability = 1 / len(scen_dict) - - if use_mpisppy: - EF_instance = sputils._create_EF_from_scen_dict( - scen_dict, - EF_name="_Q_at_theta", - # suppress_warnings=True - ) - else: - EF_instance = local_ef._create_EF_from_scen_dict( - scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True - ) - - self.ef_instance = EF_instance - # set self.model_initialized flag to True to skip extensive form model - # creation using theta_est() - self.model_initialized = True - - # return initialized theta values - if len(thetavals) == 0: - # use appropriate theta_names member - theta_ref = self._return_theta_names() - for i, theta in enumerate(theta_ref): - thetavals[theta] = theta_init_vals[i]() - - return retval, thetavals, WorstStatus - - def _get_sample_list(self, samplesize, num_samples, replacement=True): - samplelist = list() - - scenario_numbers = list(range(len(self.callback_data))) - - if num_samples is None: - # This could get very large - for i, l in enumerate(combinations(scenario_numbers, samplesize)): - samplelist.append((i, np.sort(l))) - else: - for i in range(num_samples): - attempts = 0 - unique_samples = 0 # check for duplicates in each sample - duplicate = False # check for duplicates between samples - while (unique_samples <= len(self._return_theta_names())) and ( - not duplicate - ): - sample = np.random.choice( - scenario_numbers, samplesize, replace=replacement - ) - sample = np.sort(sample).tolist() - unique_samples = len(np.unique(sample)) - if sample in samplelist: - duplicate = True - - attempts += 1 - if attempts > num_samples: # arbitrary timeout limit - raise RuntimeError( - """Internal error: timeout constructing - a sample, the dim of theta may be too - close to the samplesize""" - ) - - samplelist.append((i, sample)) - - return samplelist - - def theta_est( - self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None - ): - """ - Parameter estimation using all scenarios in the data - - Parameters - ---------- - solver: string, optional - Currently only "ef_ipopt" is supported. Default is "ef_ipopt". - return_values: list, optional - List of Variable names, used to return values from the model for data reconciliation - calc_cov: boolean, optional - If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) - cov_n: int, optional - If calc_cov=True, then the user needs to supply the number of datapoints - that are used in the objective function - - Returns - ------- - objectiveval: float - The objective function value - thetavals: pd.Series - Estimated values for theta - variable values: pd.DataFrame - Variable values for each variable name in return_values (only for solver='ef_ipopt') - cov: pd.DataFrame - Covariance matrix of the fitted parameters (only for solver='ef_ipopt') - """ - assert isinstance(solver, str) - assert isinstance(return_values, list) - assert isinstance(calc_cov, bool) - if calc_cov: - assert isinstance( - cov_n, int - ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" - assert cov_n > len( - self._return_theta_names() - ), "The number of datapoints must be greater than the number of parameters to estimate" - - return self._Q_opt( - solver=solver, - return_values=return_values, - bootlist=None, - calc_cov=calc_cov, - cov_n=cov_n, - ) - - def theta_est_bootstrap( - self, - bootstrap_samples, - samplesize=None, - replacement=True, - seed=None, - return_samples=False, - ): - """ - Parameter estimation using bootstrap resampling of the data - - Parameters - ---------- - bootstrap_samples: int - Number of bootstrap samples to draw from the data - samplesize: int or None, optional - Size of each bootstrap sample. If samplesize=None, samplesize will be - set to the number of samples in the data - replacement: bool, optional - Sample with or without replacement - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers used in each bootstrap estimation - - Returns - ------- - bootstrap_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers used in each estimation - """ - assert isinstance(bootstrap_samples, int) - assert isinstance(samplesize, (type(None), int)) - assert isinstance(replacement, bool) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - if samplesize is None: - samplesize = len(self.callback_data) - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) - - task_mgr = utils.ParallelTaskManager(bootstrap_samples) - local_list = task_mgr.global_to_local_data(global_list) - - bootstrap_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt(bootlist=list(sample)) - thetavals['samples'] = sample - bootstrap_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) - bootstrap_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del bootstrap_theta['samples'] - - return bootstrap_theta - - def theta_est_leaveNout( - self, lNo, lNo_samples=None, seed=None, return_samples=False - ): - """ - Parameter estimation where N data points are left out of each sample - - Parameters - ---------- - lNo: int - Number of data points to leave out for parameter estimation - lNo_samples: int - Number of leave-N-out samples. If lNo_samples=None, the maximum - number of combinations will be used - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers that were left out - - Returns - ------- - lNo_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers left out of each estimation - """ - assert isinstance(lNo, int) - assert isinstance(lNo_samples, (type(None), int)) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - samplesize = len(self.callback_data) - lNo - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) - - task_mgr = utils.ParallelTaskManager(len(global_list)) - local_list = task_mgr.global_to_local_data(global_list) - - lNo_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt(bootlist=list(sample)) - lNo_s = list(set(range(len(self.callback_data))) - set(sample)) - thetavals['lNo'] = np.sort(lNo_s) - lNo_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) - lNo_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del lNo_theta['lNo'] - - return lNo_theta - - def leaveNout_bootstrap_test( - self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None - ): - """ - Leave-N-out bootstrap test to compare theta values where N data points are - left out to a bootstrap analysis using the remaining data, - results indicate if theta is within a confidence region - determined by the bootstrap analysis - - Parameters - ---------- - lNo: int - Number of data points to leave out for parameter estimation - lNo_samples: int - Leave-N-out sample size. If lNo_samples=None, the maximum number - of combinations will be used - bootstrap_samples: int: - Bootstrap sample size - distribution: string - Statistical distribution used to define a confidence region, - options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, - and 'Rect' for rectangular. - alphas: list - List of alpha values used to determine if theta values are inside - or outside the region. - seed: int or None, optional - Random seed - - Returns - ---------- - List of tuples with one entry per lNo_sample: - - * The first item in each tuple is the list of N samples that are left - out. - * The second item in each tuple is a DataFrame of theta estimated using - the N samples. - * The third item in each tuple is a DataFrame containing results from - the bootstrap analysis using the remaining samples. - - For each DataFrame a column is added for each value of alpha which - indicates if the theta estimate is in (True) or out (False) of the - alpha region for a given distribution (based on the bootstrap results) - """ - assert isinstance(lNo, int) - assert isinstance(lNo_samples, (type(None), int)) - assert isinstance(bootstrap_samples, int) - assert distribution in ['Rect', 'MVN', 'KDE'] - assert isinstance(alphas, list) - assert isinstance(seed, (type(None), int)) - - if seed is not None: - np.random.seed(seed) - - data = self.callback_data.copy() - - global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) - - results = [] - for idx, sample in global_list: - # Reset callback_data to only include the sample - self.callback_data = [data[i] for i in sample] - - obj, theta = self.theta_est() - - # Reset callback_data to include all scenarios except the sample - self.callback_data = [data[i] for i in range(len(data)) if i not in sample] - - bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) - - training, test = self.confidence_region_test( - bootstrap_theta, - distribution=distribution, - alphas=alphas, - test_theta_values=theta, - ) - - results.append((sample, test, training)) - - # Reset callback_data (back to full data set) - self.callback_data = data - - return results - - def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): - """ - Objective value for each theta - - Parameters - ---------- - theta_values: pd.DataFrame, columns=theta_names - Values of theta used to compute the objective - - initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True - - - Returns - ------- - obj_at_theta: pd.DataFrame - Objective value for each theta (infeasible solutions are - omitted). - """ - if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': - pass # skip assertion if model has no fitted parameters - else: - # create a local instance of the pyomo model to access model variables and parameters - model_temp = self._create_parmest_model(self.callback_data[0]) - model_theta_list = [] # list to store indexed and non-indexed parameters - # iterate over original theta_names - for theta_i in self.theta_names: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - # check if theta in theta_names are indexed - try: - # get component UID of Set over which theta is defined - set_cuid = ComponentUID(var_validate.index_set()) - # access and iterate over the Set to generate theta names as they appear - # in the pyomo model - set_validate = set_cuid.find_component_on(model_temp) - for s in set_validate: - self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" - # generate list of theta names - model_theta_list.append(self_theta_temp) - # if theta is not indexed, copy theta name to list as-is - except AttributeError: - self_theta_temp = repr(var_cuid) - model_theta_list.append(self_theta_temp) - except: - raise - # if self.theta_names is not the same as temp model_theta_list, - # create self.theta_names_updated - if set(self.theta_names) == set(model_theta_list) and len( - self.theta_names - ) == set(model_theta_list): - pass - else: - self.theta_names_updated = model_theta_list - - if theta_values is None: - all_thetas = {} # dictionary to store fitted variables - # use appropriate theta names member - theta_names = self._return_theta_names() - else: - assert isinstance(theta_values, pd.DataFrame) - # for parallel code we need to use lists and dicts in the loop - theta_names = theta_values.columns - # # check if theta_names are in model - for theta in list(theta_names): - theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - - assert theta_temp in [ - t.replace("'", "") for t in model_theta_list - ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( - theta_temp, model_theta_list - ) - assert len(list(theta_names)) == len(model_theta_list) - - all_thetas = theta_values.to_dict('records') - - if all_thetas: - task_mgr = utils.ParallelTaskManager(len(all_thetas)) - local_thetas = task_mgr.global_to_local_data(all_thetas) - else: - if initialize_parmest_model: - task_mgr = utils.ParallelTaskManager( - 1 - ) # initialization performed using just 1 set of theta values - # walk over the mesh, return objective function - all_obj = list() - if len(all_thetas) > 0: - for Theta in local_thetas: - obj, thetvals, worststatus = self._Q_at_theta( - Theta, initialize_parmest_model=initialize_parmest_model - ) - if worststatus != pyo.TerminationCondition.infeasible: - all_obj.append(list(Theta.values()) + [obj]) - # DLW, Aug2018: should we also store the worst solver status? - else: - obj, thetvals, worststatus = self._Q_at_theta( - thetavals={}, initialize_parmest_model=initialize_parmest_model - ) - if worststatus != pyo.TerminationCondition.infeasible: - all_obj.append(list(thetvals.values()) + [obj]) - - global_all_obj = task_mgr.allgather_global_data(all_obj) - dfcols = list(theta_names) + ['obj'] - obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) - return obj_at_theta - - def likelihood_ratio_test( - self, obj_at_theta, obj_value, alphas, return_thresholds=False - ): - r""" - Likelihood ratio test to identify theta values within a confidence - region using the :math:`\chi^2` distribution - - Parameters - ---------- - obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' - Objective values for each theta value (returned by - objective_at_theta) - obj_value: int or float - Objective value from parameter estimation using all data - alphas: list - List of alpha values to use in the chi2 test - return_thresholds: bool, optional - Return the threshold value for each alpha - - Returns - ------- - LR: pd.DataFrame - Objective values for each theta value along with True or False for - each alpha - thresholds: pd.Series - If return_threshold = True, the thresholds are also returned. - """ - assert isinstance(obj_at_theta, pd.DataFrame) - assert isinstance(obj_value, (int, float)) - assert isinstance(alphas, list) - assert isinstance(return_thresholds, bool) - - LR = obj_at_theta.copy() - S = len(self.callback_data) - thresholds = {} - for a in alphas: - chi2_val = scipy.stats.chi2.ppf(a, 2) - thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) - LR[a] = LR['obj'] < thresholds[a] - - thresholds = pd.Series(thresholds) - - if return_thresholds: - return LR, thresholds - else: - return LR - - def confidence_region_test( - self, theta_values, distribution, alphas, test_theta_values=None - ): - """ - Confidence region test to determine if theta values are within a - rectangular, multivariate normal, or Gaussian kernel density distribution - for a range of alpha values - - Parameters - ---------- - theta_values: pd.DataFrame, columns = theta_names - Theta values used to generate a confidence region - (generally returned by theta_est_bootstrap) - distribution: string - Statistical distribution used to define a confidence region, - options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, - and 'Rect' for rectangular. - alphas: list - List of alpha values used to determine if theta values are inside - or outside the region. - test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional - Additional theta values that are compared to the confidence region - to determine if they are inside or outside. - - Returns - training_results: pd.DataFrame - Theta value used to generate the confidence region along with True - (inside) or False (outside) for each alpha - test_results: pd.DataFrame - If test_theta_values is not None, returns test theta value along - with True (inside) or False (outside) for each alpha - """ - assert isinstance(theta_values, pd.DataFrame) - assert distribution in ['Rect', 'MVN', 'KDE'] - assert isinstance(alphas, list) - assert isinstance( - test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) - ) - - if isinstance(test_theta_values, (dict, pd.Series)): - test_theta_values = pd.Series(test_theta_values).to_frame().transpose() - - training_results = theta_values.copy() - - if test_theta_values is not None: - test_result = test_theta_values.copy() - - for a in alphas: - if distribution == 'Rect': - lb, ub = graphics.fit_rect_dist(theta_values, a) - training_results[a] = (theta_values > lb).all(axis=1) & ( - theta_values < ub - ).all(axis=1) - - if test_theta_values is not None: - # use upper and lower bound from the training set - test_result[a] = (test_theta_values > lb).all(axis=1) & ( - test_theta_values < ub - ).all(axis=1) - - elif distribution == 'MVN': - dist = graphics.fit_mvn_dist(theta_values) - Z = dist.pdf(theta_values) - score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) - training_results[a] = Z >= score - - if test_theta_values is not None: - # use score from the training set - Z = dist.pdf(test_theta_values) - test_result[a] = Z >= score - - elif distribution == 'KDE': - dist = graphics.fit_kde_dist(theta_values) - Z = dist.pdf(theta_values.transpose()) - score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) - training_results[a] = Z >= score - - if test_theta_values is not None: - # use score from the training set - Z = dist.pdf(test_theta_values.transpose()) - test_result[a] = Z >= score - - if test_theta_values is not None: - return training_results, test_result - else: - return training_results diff --git a/pyomo/contrib/parmest/deprecated/scenariocreator.py b/pyomo/contrib/parmest/deprecated/scenariocreator.py deleted file mode 100644 index af084d0712c..00000000000 --- a/pyomo/contrib/parmest/deprecated/scenariocreator.py +++ /dev/null @@ -1,166 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# ScenariosCreator.py - Class to create and deliver scenarios using parmest -# DLW March 2020 - -import pyomo.environ as pyo - - -class ScenarioSet(object): - """ - Class to hold scenario sets - - Args: - name (str): name of the set (might be "") - - """ - - def __init__(self, name): - # Note: If there was a use-case, the list could be a dataframe. - self._scens = list() # use a df instead? - self.name = name # might be "" - - def _firstscen(self): - # Return the first scenario for testing and to get Theta names. - assert len(self._scens) > 0 - return self._scens[0] - - def ScensIterator(self): - """Usage: for scenario in ScensIterator()""" - return iter(self._scens) - - def ScenarioNumber(self, scennum): - """Returns the scenario with the given, zero-based number""" - return self._scens[scennum] - - def addone(self, scen): - """Add a scenario to the set - - Args: - scen (ParmestScen): the scenario to add - """ - assert isinstance(self._scens, list) - self._scens.append(scen) - - def append_bootstrap(self, bootstrap_theta): - """Append a bootstrap theta df to the scenario set; equally likely - - Args: - bootstrap_theta (dataframe): created by the bootstrap - Note: this can be cleaned up a lot with the list becomes a df, - which is why I put it in the ScenarioSet class. - """ - assert len(bootstrap_theta) > 0 - prob = 1.0 / len(bootstrap_theta) - - # dict of ThetaVal dicts - dfdict = bootstrap_theta.to_dict(orient='index') - - for index, ThetaVals in dfdict.items(): - name = "Bootstrap" + str(index) - self.addone(ParmestScen(name, ThetaVals, prob)) - - def write_csv(self, filename): - """write a csv file with the scenarios in the set - - Args: - filename (str): full path and full name of file - """ - if len(self._scens) == 0: - print("Empty scenario set, not writing file={}".format(filename)) - return - with open(filename, "w") as f: - f.write("Name,Probability") - for n in self._firstscen().ThetaVals.keys(): - f.write(",{}".format(n)) - f.write('\n') - for s in self.ScensIterator(): - f.write("{},{}".format(s.name, s.probability)) - for v in s.ThetaVals.values(): - f.write(",{}".format(v)) - f.write('\n') - - -class ParmestScen(object): - """A little container for scenarios; the Args are the attributes. - - Args: - name (str): name for reporting; might be "" - ThetaVals (dict): ThetaVals[name]=val - probability (float): probability of occurrence "near" these ThetaVals - """ - - def __init__(self, name, ThetaVals, probability): - self.name = name - assert isinstance(ThetaVals, dict) - self.ThetaVals = ThetaVals - self.probability = probability - - -############################################################ - - -class ScenarioCreator(object): - """Create scenarios from parmest. - - Args: - pest (Estimator): the parmest object - solvername (str): name of the solver (e.g. "ipopt") - - """ - - def __init__(self, pest, solvername): - self.pest = pest - self.solvername = solvername - - def ScenariosFromExperiments(self, addtoSet): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - Returns: - a ScenarioSet - """ - - # assert isinstance(addtoSet, ScenarioSet) - - scenario_numbers = list(range(len(self.pest.callback_data))) - - prob = 1.0 / len(scenario_numbers) - for exp_num in scenario_numbers: - ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data - ) - opt = pyo.SolverFactory(self.solvername) - results = opt.solve(model) # solves and updates model - ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval - addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) - - def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - numtomake (int) : number of scenarios to create - """ - - # assert isinstance(addtoSet, ScenarioSet) - - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) - addtoSet.append_bootstrap(bootstrap_thetas) diff --git a/pyomo/contrib/parmest/deprecated/tests/__init__.py b/pyomo/contrib/parmest/deprecated/tests/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/tests/scenarios.csv b/pyomo/contrib/parmest/deprecated/tests/scenarios.csv deleted file mode 100644 index 22f9a651bc3..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/scenarios.csv +++ /dev/null @@ -1,11 +0,0 @@ -Name,Probability,k1,k2,E1,E2 -ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 -ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 -ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 -ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 -ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 -ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 -ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 -ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py deleted file mode 100644 index 6f5d9703f05..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_examples.py +++ /dev/null @@ -1,204 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.common.unittest as unittest -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.graphics import matplotlib_available, seaborn_available -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestRooneyBieglerExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - rooney_biegler, - ) - - rooney_biegler.main() - - def test_model_with_constraint(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - rooney_biegler_with_constraint, - ) - - rooney_biegler_with_constraint.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - parameter_estimation_example, - ) - - parameter_estimation_example.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - bootstrap_example, - ) - - bootstrap_example.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - likelihood_ratio_example, - ) - - likelihood_ratio_example.main() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactionKineticsExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - def test_example(self): - from pyomo.contrib.parmest.deprecated.examples.reaction_kinetics import ( - simple_reaction_parmest_example, - ) - - simple_reaction_parmest_example.main() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestSemibatchExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.semibatch import semibatch - - semibatch.main() - - def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.deprecated.examples.semibatch import ( - parameter_estimation_example, - ) - - parameter_estimation_example.main() - - def test_scenario_example(self): - from pyomo.contrib.parmest.deprecated.examples.semibatch import scenario_example - - scenario_example.main() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesignExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive - def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - reactor_design, - ) - - reactor_design.main() - - def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - parameter_estimation_example, - ) - - parameter_estimation_example.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - bootstrap_example, - ) - - bootstrap_example.main() - - @unittest.pytest.mark.expensive - def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - likelihood_ratio_example, - ) - - likelihood_ratio_example.main() - - @unittest.pytest.mark.expensive - def test_leaveNout_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - leaveNout_example, - ) - - leaveNout_example.main() - - def test_timeseries_data_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - timeseries_data_example, - ) - - timeseries_data_example.main() - - def test_multisensor_data_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - multisensor_data_example, - ) - - multisensor_data_example.main() - - @unittest.skipUnless(matplotlib_available, "test requires matplotlib") - def test_datarec_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - datarec_example, - ) - - datarec_example.main() - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_graphics.py b/pyomo/contrib/parmest/deprecated/tests/test_graphics.py deleted file mode 100644 index c18659e9948..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_graphics.py +++ /dev/null @@ -1,68 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - -import platform - -is_osx = platform.mac_ver()[0] != '' - -import pyomo.common.unittest as unittest -import sys -import os - -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest.graphics as graphics - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf( - not graphics.imports_available, "parmest.graphics imports are unavailable" -) -@unittest.skipIf( - is_osx, - "Disabling graphics tests on OSX due to issue in Matplotlib, see Pyomo PR #1337", -) -class TestGraphics(unittest.TestCase): - def setUp(self): - self.A = pd.DataFrame( - np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') - ) - self.B = pd.DataFrame( - np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') - ) - - def test_pairwise_plot(self): - graphics.pairwise_plot(self.A, alpha=0.8, distributions=['Rect', 'MVN', 'KDE']) - - def test_grouped_boxplot(self): - graphics.grouped_boxplot(self.A, self.B, normalize=True, group_names=['A', 'B']) - - def test_grouped_violinplot(self): - graphics.grouped_violinplot(self.A, self.B) - - -if __name__ == '__main__': - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py deleted file mode 100644 index 27776bdc64c..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py +++ /dev/null @@ -1,956 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - -import platform - -is_osx = platform.mac_ver()[0] != "" - -import pyomo.common.unittest as unittest -import sys -import os -import subprocess -from itertools import product - -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest.graphics as graphics -import pyomo.contrib.parmest as parmestbase -import pyomo.environ as pyo -import pyomo.dae as dae - -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - -from pyomo.common.fileutils import find_library - -pynumero_ASL_available = False if find_library("pynumero_ASL") is None else True - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestRooneyBiegler(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, - ) - - # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=["hour", "y"], - ) - - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) - return expr - - solver_options = {"tol": 1e-8} - - self.data = data - self.pest = parmest.Estimator( - rooney_biegler_model, - data, - theta_names, - SSE, - solver_options=solver_options, - tee=True, - ) - - def test_theta_est(self): - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - @unittest.skipIf( - not graphics.imports_available, "parmest.graphics imports are unavailable" - ) - def test_bootstrap(self): - objval, thetavals = self.pest.theta_est() - - num_bootstraps = 10 - theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) - - num_samples = theta_est["samples"].apply(len) - self.assertTrue(len(theta_est.index), 10) - self.assertTrue(num_samples.equals(pd.Series([6] * 10))) - - del theta_est["samples"] - - # apply confidence region test - CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) - - self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) - self.assertTrue(CR[0.5].sum() == 5) - self.assertTrue(CR[0.75].sum() == 7) - self.assertTrue(CR[1.0].sum() == 10) # all true - - graphics.pairwise_plot(theta_est) - graphics.pairwise_plot(theta_est, thetavals) - graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) - - @unittest.skipIf( - not graphics.imports_available, "parmest.graphics imports are unavailable" - ) - def test_likelihood_ratio(self): - objval, thetavals = self.pest.theta_est() - - asym = np.arange(10, 30, 2) - rate = np.arange(0, 1.5, 0.25) - theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest._return_theta_names() - ) - - obj_at_theta = self.pest.objective_at_theta(theta_vals) - - LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) - - self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) - self.assertTrue(LR[0.8].sum() == 6) - self.assertTrue(LR[0.9].sum() == 10) - self.assertTrue(LR[1.0].sum() == 60) # all true - - graphics.pairwise_plot(LR, thetavals, 0.8) - - def test_leaveNout(self): - lNo_theta = self.pest.theta_est_leaveNout(1) - self.assertTrue(lNo_theta.shape == (6, 2)) - - results = self.pest.leaveNout_bootstrap_test( - 1, None, 3, "Rect", [0.5, 1.0], seed=5436 - ) - self.assertTrue(len(results) == 6) # 6 lNo samples - i = 1 - samples = results[i][0] # list of N samples that are left out - lno_theta = results[i][1] - bootstrap_theta = results[i][2] - self.assertTrue(samples == [1]) # sample 1 was left out - self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 - self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) - self.assertTrue(lno_theta[1.0].sum() == 1) # all true - self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 - self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true - - def test_diagnostic_mode(self): - self.pest.diagnostic_mode = True - - objval, thetavals = self.pest.theta_est() - - asym = np.arange(10, 30, 2) - rate = np.arange(0, 1.5, 0.25) - theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest._return_theta_names() - ) - - obj_at_theta = self.pest.objective_at_theta(theta_vals) - - self.pest.diagnostic_mode = False - - @unittest.skip("Presently having trouble with mpiexec on appveyor") - def test_parallel_parmest(self): - """use mpiexec and mpi4py""" - p = str(parmestbase.__path__) - l = p.find("'") - r = p.find("'", l + 1) - parmestpath = p[l + 1 : r] - rbpath = ( - parmestpath - + os.sep - + "examples" - + os.sep - + "rooney_biegler" - + os.sep - + "rooney_biegler_parmest.py" - ) - rbpath = os.path.abspath(rbpath) # paranoia strikes deep... - rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] - if sys.version_info >= (3, 5): - ret = subprocess.run(rlist) - retcode = ret.returncode - else: - retcode = subprocess.call(rlist) - assert retcode == 0 - - @unittest.skip("Most folks don't have k_aug installed") - def test_theta_k_aug_for_Hessian(self): - # this will fail if k_aug is not installed - objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") - self.assertAlmostEqual(objval, 4.4675, places=2) - - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) - def test_theta_est_cov(self): - objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - # Covariance matrix - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper - - """ Why does the covariance matrix from parmest not match the paper? Parmest is - calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely - employed the first order approximation common for nonlinear regression. The paper - values were verified with Scipy, which uses the same first order approximation. - The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in - "Nonlinear Parameter Estimation", Y. Bard, 1974. - """ - - def test_cov_scipy_least_squares_comparison(self): - """ - Scipy results differ in the 3rd decimal place from the paper. It is possible - the paper used an alternative finite difference approximation for the Jacobian. - """ - - def model(theta, t): - """ - Model to be fitted y = model(theta, t) - Arguments: - theta: vector of fitted parameters - t: independent variable [hours] - - Returns: - y: model predictions [need to check paper for units] - """ - asymptote = theta[0] - rate_constant = theta[1] - - return asymptote * (1 - np.exp(-rate_constant * t)) - - def residual(theta, t, y): - """ - Calculate residuals - Arguments: - theta: vector of fitted parameters - t: independent variable [hours] - y: dependent variable [?] - """ - return y - model(theta, t) - - # define data - t = self.data["hour"].to_numpy() - y = self.data["y"].to_numpy() - - # define initial guess - theta_guess = np.array([15, 0.5]) - - ## solve with optimize.least_squares - sol = scipy.optimize.least_squares( - residual, theta_guess, method="trf", args=(t, y), verbose=2 - ) - theta_hat = sol.x - - self.assertAlmostEqual( - theta_hat[0], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper - - # calculate residuals - r = residual(theta_hat, t, y) - - # calculate variance of the residuals - # -2 because there are 2 fitted parameters - sigre = np.matmul(r.T, r / (len(y) - 2)) - - # approximate covariance - # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 - cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) - - self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper - self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper - - def test_cov_scipy_curve_fit_comparison(self): - """ - Scipy results differ in the 3rd decimal place from the paper. It is possible - the paper used an alternative finite difference approximation for the Jacobian. - """ - - ## solve with optimize.curve_fit - def model(t, asymptote, rate_constant): - return asymptote * (1 - np.exp(-rate_constant * t)) - - # define data - t = self.data["hour"].to_numpy() - y = self.data["y"].to_numpy() - - # define initial guess - theta_guess = np.array([15, 0.5]) - - theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) - - self.assertAlmostEqual( - theta_hat[0], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper - - self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper - self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestModelVariants(unittest.TestCase): - def setUp(self): - self.data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=["hour", "y"], - ) - - def rooney_biegler_params(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Param(initialize=15, mutable=True) - model.rate_constant = pyo.Param(initialize=0.5, mutable=True) - - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def rooney_biegler_indexed_params(data): - model = pyo.ConcreteModel() - - model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) - model.theta = pyo.Param( - model.param_names, - initialize={"asymptote": 15, "rate_constant": 0.5}, - mutable=True, - ) - - def response_rule(m, h): - expr = m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * h) - ) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def rooney_biegler_vars(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Var(initialize=15) - model.rate_constant = pyo.Var(initialize=0.5) - model.asymptote.fixed = True # parmest will unfix theta variables - model.rate_constant.fixed = True - - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def rooney_biegler_indexed_vars(data): - model = pyo.ConcreteModel() - - model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) - model.theta = pyo.Var( - model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} - ) - model.theta["asymptote"].fixed = ( - True # parmest will unfix theta variables, even when they are indexed - ) - model.theta["rate_constant"].fixed = True - - def response_rule(m, h): - expr = m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * h) - ) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) - return expr - - self.objective_function = SSE - - theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T - theta_vals_index = pd.DataFrame( - [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] - ).T - - self.input = { - "param": { - "model": rooney_biegler_params, - "theta_names": ["asymptote", "rate_constant"], - "theta_vals": theta_vals, - }, - "param_index": { - "model": rooney_biegler_indexed_params, - "theta_names": ["theta"], - "theta_vals": theta_vals_index, - }, - "vars": { - "model": rooney_biegler_vars, - "theta_names": ["asymptote", "rate_constant"], - "theta_vals": theta_vals, - }, - "vars_index": { - "model": rooney_biegler_indexed_vars, - "theta_names": ["theta"], - "theta_vals": theta_vals_index, - }, - "vars_quoted_index": { - "model": rooney_biegler_indexed_vars, - "theta_names": ["theta['asymptote']", "theta['rate_constant']"], - "theta_vals": theta_vals_index, - }, - "vars_str_index": { - "model": rooney_biegler_indexed_vars, - "theta_names": ["theta[asymptote]", "theta[rate_constant]"], - "theta_vals": theta_vals_index, - }, - } - - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) - def test_parmest_basics(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) - self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - - def test_parmest_basics_with_initialize_parmest_model_option(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - obj_at_theta = pest.objective_at_theta( - parmest_input["theta_vals"], initialize_parmest_model=True - ) - - self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - - def test_parmest_basics_with_square_problem_solve(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - obj_at_theta = pest.objective_at_theta( - parmest_input["theta_vals"], initialize_parmest_model=True - ) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - - def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesign(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) - - # Data from the design - data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], - [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], - [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], - [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], - [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], - [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], - [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], - [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], - [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], - [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], - [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], - [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], - [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], - [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], - [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], - [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], - ) - - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - solver_options = {"max_iter": 6000} - - self.pest = parmest.Estimator( - reactor_design_model, data, theta_names, SSE, solver_options=solver_options - ) - - def test_theta_est(self): - # used in data reconciliation - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) - self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) - self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) - - def test_return_values(self): - objval, thetavals, data_rec = self.pest.theta_est( - return_values=["ca", "cb", "cc", "cd", "caf"] - ) - self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesign_DAE(unittest.TestCase): - # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, - # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ - # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html - - def setUp(self): - def ABC_model(data): - ca_meas = data["ca"] - cb_meas = data["cb"] - cc_meas = data["cc"] - - if isinstance(data, pd.DataFrame): - meas_t = data.index # time index - else: # dictionary - meas_t = list(ca_meas.keys()) # nested dictionary - - ca0 = 1.0 - cb0 = 0.0 - cc0 = 0.0 - - m = pyo.ConcreteModel() - - m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) - m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) - - m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) - - # initialization and bounds - m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) - m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) - m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) - - m.dca = dae.DerivativeVar(m.ca, wrt=m.time) - m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) - m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) - - def _dcarate(m, t): - if t == 0: - return pyo.Constraint.Skip - else: - return m.dca[t] == -m.k1 * m.ca[t] - - m.dcarate = pyo.Constraint(m.time, rule=_dcarate) - - def _dcbrate(m, t): - if t == 0: - return pyo.Constraint.Skip - else: - return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] - - m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) - - def _dccrate(m, t): - if t == 0: - return pyo.Constraint.Skip - else: - return m.dcc[t] == m.k2 * m.cb[t] - - m.dccrate = pyo.Constraint(m.time, rule=_dccrate) - - def ComputeFirstStageCost_rule(m): - return 0 - - m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) - - def ComputeSecondStageCost_rule(m): - return sum( - (m.ca[t] - ca_meas[t]) ** 2 - + (m.cb[t] - cb_meas[t]) ** 2 - + (m.cc[t] - cc_meas[t]) ** 2 - for t in meas_t - ) - - m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) - - def total_cost_rule(model): - return model.FirstStageCost + model.SecondStageCost - - m.Total_Cost_Objective = pyo.Objective( - rule=total_cost_rule, sense=pyo.minimize - ) - - disc = pyo.TransformationFactory("dae.collocation") - disc.apply_to(m, nfe=20, ncp=2) - - return m - - # This example tests data formatted in 3 ways - # Each format holds 1 scenario - # 1. dataframe with time index - # 2. nested dictionary {ca: {t, val pairs}, ... } - data = [ - [0.000, 0.957, -0.031, -0.015], - [0.263, 0.557, 0.330, 0.044], - [0.526, 0.342, 0.512, 0.156], - [0.789, 0.224, 0.499, 0.310], - [1.053, 0.123, 0.428, 0.454], - [1.316, 0.079, 0.396, 0.556], - [1.579, 0.035, 0.303, 0.651], - [1.842, 0.029, 0.287, 0.658], - [2.105, 0.025, 0.221, 0.750], - [2.368, 0.017, 0.148, 0.854], - [2.632, -0.002, 0.182, 0.845], - [2.895, 0.009, 0.116, 0.893], - [3.158, -0.023, 0.079, 0.942], - [3.421, 0.006, 0.078, 0.899], - [3.684, 0.016, 0.059, 0.942], - [3.947, 0.014, 0.036, 0.991], - [4.211, -0.009, 0.014, 0.988], - [4.474, -0.030, 0.036, 0.941], - [4.737, 0.004, 0.036, 0.971], - [5.000, -0.024, 0.028, 0.985], - ] - data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) - data_df = data.set_index("t") - data_dict = { - "ca": {k: v for (k, v) in zip(data.t, data.ca)}, - "cb": {k: v for (k, v) in zip(data.t, data.cb)}, - "cc": {k: v for (k, v) in zip(data.t, data.cc)}, - } - - theta_names = ["k1", "k2"] - - self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) - self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) - - # Estimator object with multiple scenarios - self.pest_df_multiple = parmest.Estimator( - ABC_model, [data_df, data_df], theta_names - ) - self.pest_dict_multiple = parmest.Estimator( - ABC_model, [data_dict, data_dict], theta_names - ) - - # Create an instance of the model - self.m_df = ABC_model(data_df) - self.m_dict = ABC_model(data_dict) - - def test_dataformats(self): - obj1, theta1 = self.pest_df.theta_est() - obj2, theta2 = self.pest_dict.theta_est() - - self.assertAlmostEqual(obj1, obj2, places=6) - self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) - self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) - - def test_return_continuous_set(self): - """ - test if ContinuousSet elements are returned correctly from theta_est() - """ - obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) - obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) - self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) - self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) - - def test_return_continuous_set_multiple_datasets(self): - """ - test if ContinuousSet elements are returned correctly from theta_est() - """ - obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( - return_values=["time"] - ) - obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( - return_values=["time"] - ) - self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) - self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) - - def test_covariance(self): - from pyomo.contrib.interior_point.inverse_reduced_hessian import ( - inv_reduced_hessian_barrier, - ) - - # Number of datapoints. - # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 - # In this example, this is the number of data points in data_df, but that's - # only because the data is indexed by time and contains no additional information. - n = 60 - - # Compute covariance using parmest - obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) - - # Compute covariance using interior_point - vars_list = [self.m_df.k1, self.m_df.k2] - solve_result, inv_red_hes = inv_reduced_hessian_barrier( - self.m_df, independent_variables=vars_list, tee=True - ) - l = len(vars_list) - cov_interior_point = 2 * obj / (n - l) * inv_red_hes - cov_interior_point = pd.DataFrame( - cov_interior_point, ["k1", "k2"], ["k1", "k2"] - ) - - cov_diff = (cov - cov_interior_point).abs().sum().sum() - - self.assertTrue(cov.loc["k1", "k1"] > 0) - self.assertTrue(cov.loc["k2", "k2"] > 0) - self.assertAlmostEqual(cov_diff, 0, places=6) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestSquareInitialization_RooneyBiegler(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler_with_constraint import ( - rooney_biegler_model_with_constraint, - ) - - # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=["hour", "y"], - ) - - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) - return expr - - solver_options = {"tol": 1e-8} - - self.data = data - self.pest = parmest.Estimator( - rooney_biegler_model_with_constraint, - data, - theta_names, - SSE, - solver_options=solver_options, - tee=True, - ) - - def test_theta_est_with_square_initialization(self): - obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - def test_theta_est_with_square_initialization_and_custom_init_theta(self): - theta_vals_init = pd.DataFrame( - data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] - ) - obj_init = self.pest.objective_at_theta( - theta_values=theta_vals_init, initialize_parmest_model=True - ) - objval, thetavals = self.pest.theta_est() - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - def test_theta_est_with_square_initialization_diagnostic_mode_true(self): - self.pest.diagnostic_mode = True - obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - self.pest.diagnostic_mode = False - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py deleted file mode 100644 index 54cbe80f73c..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py +++ /dev/null @@ -1,146 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import pandas as pd, pandas_available - -uuid_available = True -try: - import uuid -except: - uuid_available = False - -import pyomo.common.unittest as unittest -import os -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest.scenariocreator as sc -import pyomo.environ as pyo -from pyomo.environ import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestScenarioReactorDesign(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) - - # Data from the design - data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], - [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], - [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], - [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], - [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], - [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], - [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], - [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], - [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], - [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], - [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], - [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], - [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], - [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], - [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], - [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], - ) - - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - def test_scen_from_exps(self): - scenmaker = sc.ScenarioCreator(self.pest, "ipopt") - experimentscens = sc.ScenarioSet("Experiments") - scenmaker.ScenariosFromExperiments(experimentscens) - experimentscens.write_csv("delme_exp_csv.csv") - df = pd.read_csv("delme_exp_csv.csv") - os.remove("delme_exp_csv.csv") - # March '20: all reactor_design experiments have the same theta values! - k1val = df.loc[5].at["k1"] - self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) - tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] - self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) - - @unittest.skipIf(not uuid_available, "The uuid module is not available") - def test_no_csv_if_empty(self): - # low level test of scenario sets - # verify that nothing is written, but no errors with empty set - - emptyset = sc.ScenarioSet("empty") - tfile = uuid.uuid4().hex + ".csv" - emptyset.write_csv(tfile) - self.assertFalse( - os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" - ) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestScenarioSemibatch(unittest.TestCase): - def setUp(self): - import pyomo.contrib.parmest.deprecated.examples.semibatch.semibatch as sb - import json - - # Vars to estimate in parmest - theta_names = ["k1", "k2", "E1", "E2"] - - self.fbase = os.path.join(testdir, "..", "examples", "semibatch") - # Data, list of dictionaries - data = [] - for exp_num in range(10): - fname = "exp" + str(exp_num + 1) + ".out" - fullname = os.path.join(self.fbase, fname) - with open(fullname, "r") as infile: - d = json.load(infile) - data.append(d) - - # Note, the model already includes a 'SecondStageCost' expression - # for the sum of squared error that will be used in parameter estimation - - self.pest = parmest.Estimator(sb.generate_model, data, theta_names) - - def test_semibatch_bootstrap(self): - scenmaker = sc.ScenarioCreator(self.pest, "ipopt") - bootscens = sc.ScenarioSet("Bootstrap") - numtomake = 2 - scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) - tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] - self.assertAlmostEqual(tval, 20.64, places=1) - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_solver.py b/pyomo/contrib/parmest/deprecated/tests/test_solver.py deleted file mode 100644 index eb655023b9b..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_solver.py +++ /dev/null @@ -1,75 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - -import platform - -is_osx = platform.mac_ver()[0] != '' - -import pyomo.common.unittest as unittest -import os - -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest as parmestbase -import pyomo.environ as pyo - -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory('ipopt').available() - -from pyomo.common.fileutils import find_library - -pynumero_ASL_available = False if find_library('pynumero_ASL') is None else True - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestSolver(unittest.TestCase): - def setUp(self): - pass - - def test_ipopt_solve_with_stats(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, - ) - from pyomo.contrib.parmest.utils import ipopt_solve_with_stats - - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - model = rooney_biegler_model(data) - solver = pyo.SolverFactory('ipopt') - solver.solve(model) - - status_obj, solved, iters, time, regu = ipopt_solve_with_stats(model, solver) - - self.assertEqual(solved, True) - - -if __name__ == '__main__': - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_utils.py b/pyomo/contrib/parmest/deprecated/tests/test_utils.py deleted file mode 100644 index 1a8247ddcc9..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_utils.py +++ /dev/null @@ -1,68 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import pandas as pd, pandas_available - -import pyomo.environ as pyo -import pyomo.common.unittest as unittest -import pyomo.contrib.parmest.parmest as parmest -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestUtils(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive - def test_convert_param_to_var(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) - - data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], - ) - - theta_names = ["k1", "k2", "k3"] - - instance = reactor_design_model(data.loc[0]) - solver = pyo.SolverFactory("ipopt") - solver.solve(instance) - - instance_vars = parmest.utils.convert_params_to_vars( - instance, theta_names, fix_vars=True - ) - solver.solve(instance_vars) - - assert instance.k1() == instance_vars.k1() - assert instance.k2() == instance_vars.k2() - assert instance.k3() == instance_vars.k3() - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 90d42e68910..9e5b480332d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -75,7 +75,10 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet -import pyomo.contrib.parmest.deprecated.parmest as parmest_deprecated +from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecation_warning + +DEPRECATION_VERSION = '6.7.0' parmest_available = numpy_available & pandas_available & scipy_available @@ -312,8 +315,8 @@ class Estimator(object): Provides options to the solver (also the name of an attribute) """ - # backwards compatible constructor will accept the old inputs - # from parmest_deprecated as well as the new inputs using experiment lists + # backwards compatible constructor will accept the old deprecated inputs + # as well as the new inputs using experiment lists def __init__(self, *args, **kwargs): # check that we have at least one argument @@ -322,11 +325,12 @@ def __init__(self, *args, **kwargs): # use deprecated interface self.pest_deprecated = None if callable(args[0]): - logger.warning( + deprecation_warning( 'Using deprecated parmest inputs (model_function, ' - + 'data, theta_names), please use experiment lists instead.' + + 'data, theta_names), please use experiment lists instead.', + version=DEPRECATION_VERSION, ) - self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) + self.pest_deprecated = DeprecatedEstimator(*args, **kwargs) return # check that we have a (non-empty) list of experiments @@ -1436,3 +1440,1112 @@ def confidence_region_test( return training_results, test_result else: return training_results + + +################################ +# deprecated functions/classes # +################################ + + +@deprecated(version=DEPRECATION_VERSION) +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + +class _DeprecatedSecondStageCostExpr(object): + """ + Class to pass objective expression into the Pyomo model + """ + + def __init__(self, ssc_function, data): + self._ssc_function = ssc_function + self._data = data + + def __call__(self, model): + return self._ssc_function(model, self._data) + + +class DeprecatedEstimator(object): + """ + Parameter estimation class + + Parameters + ---------- + model_function: function + Function that generates an instance of the Pyomo model using 'data' + as the input argument + data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names + Data that is used to build an instance of the Pyomo model and build + the objective function + theta_names: list of strings + List of Var names to estimate + obj_function: function, optional + Function used to formulate parameter estimation objective, generally + sum of squared error between measurements and model variables. + If no function is specified, the model is used + "as is" and should be defined with a "FirstStageCost" and + "SecondStageCost" expression that are used to build an objective. + tee: bool, optional + Indicates that ef solver output should be teed + diagnostic_mode: bool, optional + If True, print diagnostics from the solver + solver_options: dict, optional + Provides options to the solver (also the name of an attribute) + """ + + def __init__( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + self.model_function = model_function + + assert isinstance( + data, (list, pd.DataFrame) + ), "Data must be a list or DataFrame" + # convert dataframe into a list of dataframes, each row = one scenario + if isinstance(data, pd.DataFrame): + self.callback_data = [ + data.loc[i, :].to_frame().transpose() for i in data.index + ] + else: + self.callback_data = data + assert isinstance( + self.callback_data[0], (dict, pd.DataFrame, str) + ), "The scenarios in data must be a dictionary, DataFrame or filename" + + if len(theta_names) == 0: + self.theta_names = ['parmest_dummy_var'] + else: + self.theta_names = theta_names + + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options + + self._second_stage_cost_exp = "SecondStageCost" + # boolean to indicate if model is initialized using a square solve + self.model_initialized = False + + def _return_theta_names(self): + """ + Return list of fitted model parameter names + """ + # if fitted model parameter names differ from theta_names created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, data): + """ + Modify the Pyomo model for parameter estimation + """ + model = self.model_function(data) + + if (len(self.theta_names) == 1) and ( + self.theta_names[0] == 'parmest_dummy_var' + ): + model.parmest_dummy_var = pyo.Var(initialize=1.0) + + # Add objective function (optional) + if self.obj_function: + for obj in model.component_objects(pyo.Objective): + if obj.name in ["Total_Cost_Objective"]: + raise RuntimeError( + "Parmest will not override the existing model Objective named " + + obj.name + ) + obj.deactivate() + + for expr in model.component_data_objects(pyo.Expression): + if expr.name in ["FirstStageCost", "SecondStageCost"]: + raise RuntimeError( + "Parmest will not override the existing model Expression named " + + expr.name + ) + model.FirstStageCost = pyo.Expression(expr=0) + model.SecondStageCost = pyo.Expression( + rule=_DeprecatedSecondStageCostExpr(self.obj_function, data) + ) + + def TotalCost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + model.Total_Cost_Objective = pyo.Objective( + rule=TotalCost_rule, sense=pyo.minimize + ) + + # Convert theta Params to Vars, and unfix theta Vars + model = utils.convert_params_to_vars(model, self.theta_names) + + # Update theta names list to use CUID string representation + for i, theta in enumerate(self.theta_names): + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(model) + if var_validate is None: + logger.warning( + "theta_name[%s] (%s) was not found on the model", (i, theta) + ) + else: + try: + # If the component is not a variable, + # this will generate an exception (and the warning + # in the 'except') + var_validate.unfix() + self.theta_names[i] = repr(var_cuid) + except: + logger.warning(theta + ' is not a variable') + + self.parmest_model = model + + return model + + def _instance_creation_callback(self, experiment_number=None, cb_data=None): + # cb_data is a list of dictionaries, list of dataframes, OR list of json file names + exp_data = cb_data[experiment_number] + if isinstance(exp_data, (dict, pd.DataFrame)): + pass + elif isinstance(exp_data, str): + try: + with open(exp_data, 'r') as infile: + exp_data = json.load(infile) + except: + raise RuntimeError(f'Could not read {exp_data} as json') + else: + raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') + model = self._create_parmest_model(exp_data) + + return model + + def _Q_opt( + self, + ThetaVals=None, + solver="ef_ipopt", + return_values=[], + bootlist=None, + calc_cov=False, + cov_n=None, + ): + """ + Set up all thetas as first stage Vars, return resulting theta + values as well as the objective function value. + + """ + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") + + # (Bootstrap scenarios will use indirection through the bootlist) + if bootlist is None: + scenario_numbers = list(range(len(self.callback_data))) + scen_names = ["Scenario{}".format(i) for i in scenario_numbers] + else: + scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] + + # tree_model.CallbackModule = None + outer_cb_data = dict() + outer_cb_data["callback"] = self._instance_creation_callback + if ThetaVals is not None: + outer_cb_data["ThetaVals"] = ThetaVals + if bootlist is not None: + outer_cb_data["BootList"] = bootlist + outer_cb_data["cb_data"] = self.callback_data # None is OK + outer_cb_data["theta_names"] = self.theta_names + + options = {"solver": "ipopt"} + scenario_creator_options = {"cb_data": outer_cb_data} + if use_mpisppy: + ef = sputils.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + else: + ef = local_ef.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + self.ef_instance = ef + + # Solve the extensive form with ipopt + if solver == "ef_ipopt": + if not calc_cov: + # Do not calculate the reduced hessian + + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] + + solve_result = solver.solve(self.ef_instance, tee=self.tee) + + # The import error will be raised when we attempt to use + # inv_reduced_hessian_barrier below. + # + # elif not asl_available: + # raise ImportError("parmest requires ASL to calculate the " + # "covariance matrix with solver 'ipopt'") + else: + # parmest makes the fitted parameters stage 1 variables + ind_vars = [] + for ndname, Var, solval in ef_nonants(ef): + ind_vars.append(Var) + # calculate the reduced hessian + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + ) + + if self.diagnostic_mode: + print( + ' Solver termination condition = ', + str(solve_result.solver.termination_condition), + ) + + # assume all first stage are thetas... + thetavals = {} + for ndname, Var, solval in ef_nonants(ef): + # process the name + # the scenarios are blocks, so strip the scenario name + vname = Var.name[Var.name.find(".") + 1 :] + thetavals[vname] = solval + + objval = pyo.value(ef.EF_Obj) + + if calc_cov: + # Calculate the covariance matrix + + # Number of data points considered + n = cov_n + + # Extract number of fitted parameters + l = len(thetavals) + + # Assumption: Objective value is sum of squared errors + sse = objval + + '''Calculate covariance assuming experimental observation errors are + independent and follow a Gaussian + distribution with constant variance. + + The formula used in parmest was verified against equations (7-5-15) and + (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. + + This formula is also applicable if the objective is scaled by a constant; + the constant cancels out. (was scaled by 1/n because it computes an + expected value.) + ''' + cov = 2 * sse / (n - l) * inv_red_hes + cov = pd.DataFrame( + cov, index=thetavals.keys(), columns=thetavals.keys() + ) + + thetavals = pd.Series(thetavals) + + if len(return_values) > 0: + var_values = [] + if len(scen_names) > 1: # multiple scenarios + block_objects = self.ef_instance.component_objects( + Block, descend_into=False + ) + else: # single scenario + block_objects = [self.ef_instance] + for exp_i in block_objects: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if ( + exp_i_var is None + ): # we might have a block such as _mpisppy_data + continue + # if value to return is ContinuousSet + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + if calc_cov: + return objval, thetavals, var_values, cov + else: + return objval, thetavals, var_values + + if calc_cov: + return objval, thetavals, cov + else: + return objval, thetavals + + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + + def _Q_at_theta(self, thetavals, initialize_parmest_model=False): + """ + Return the objective function value with fixed theta values. + + Parameters + ---------- + thetavals: dict + A dictionary of theta values. + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + Returns + ------- + objectiveval: float + The objective function value. + thetavals: dict + A dictionary of all values for theta that were input. + solvertermination: Pyomo TerminationCondition + Tries to return the "worst" solver status across the scenarios. + pyo.TerminationCondition.optimal is the best and + pyo.TerminationCondition.infeasible is the worst. + """ + + optimizer = pyo.SolverFactory('ipopt') + + if len(thetavals) > 0: + dummy_cb = { + "callback": self._instance_creation_callback, + "ThetaVals": thetavals, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + else: + dummy_cb = { + "callback": self._instance_creation_callback, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + + if self.diagnostic_mode: + if len(thetavals) > 0: + print(' Compute objective at theta = ', str(thetavals)) + else: + print(' Compute objective at initial theta') + + # start block of code to deal with models with no constraints + # (ipopt will crash or complain on such problems without special care) + instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) + try: # deal with special problems so Ipopt will not crash + first = next(instance.component_objects(pyo.Constraint, active=True)) + active_constraints = True + except: + active_constraints = False + # end block of code to deal with models with no constraints + + WorstStatus = pyo.TerminationCondition.optimal + totobj = 0 + scenario_numbers = list(range(len(self.callback_data))) + if initialize_parmest_model: + # create dictionary to store pyomo model instances (scenarios) + scen_dict = dict() + + for snum in scenario_numbers: + sname = "scenario_NODE" + str(snum) + instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + + if initialize_parmest_model: + # list to store fitted parameter names that will be unfixed + # after initialization + theta_init_vals = [] + # use appropriate theta_names member + theta_ref = self._return_theta_names() + + for i, theta in enumerate(theta_ref): + # Use parser in ComponentUID to locate the component + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(instance) + if var_validate is None: + logger.warning( + "theta_name %s was not found on the model", (theta) + ) + else: + try: + if len(thetavals) == 0: + var_validate.fix() + else: + var_validate.fix(thetavals[theta]) + theta_init_vals.append(var_validate) + except: + logger.warning( + 'Unable to fix model parameter value for %s (not a Pyomo model Var)', + (theta), + ) + + if active_constraints: + if self.diagnostic_mode: + print(' Experiment = ', snum) + print(' First solve with special diagnostics wrapper') + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) + ) + print( + " status_obj, solved, iters, time, regularization_stat = ", + str(status_obj), + str(solved), + str(iters), + str(time), + str(regu), + ) + + results = optimizer.solve(instance) + if self.diagnostic_mode: + print( + 'standard solve solver termination condition=', + str(results.solver.termination_condition), + ) + + if ( + results.solver.termination_condition + != pyo.TerminationCondition.optimal + ): + # DLW: Aug2018: not distinguishing "middlish" conditions + if WorstStatus != pyo.TerminationCondition.infeasible: + WorstStatus = results.solver.termination_condition + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} infeasible with initialized parameter values".format( + snum + ) + ) + else: + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} initialization successful with initial parameter values".format( + snum + ) + ) + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + else: + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + + objobject = getattr(instance, self._second_stage_cost_exp) + objval = pyo.value(objobject) + totobj += objval + + retval = totobj / len(scenario_numbers) # -1?? + if initialize_parmest_model and not hasattr(self, 'ef_instance'): + # create extensive form of the model using scenario dictionary + if len(scen_dict) > 0: + for scen in scen_dict.values(): + scen._mpisppy_probability = 1 / len(scen_dict) + + if use_mpisppy: + EF_instance = sputils._create_EF_from_scen_dict( + scen_dict, + EF_name="_Q_at_theta", + # suppress_warnings=True + ) + else: + EF_instance = local_ef._create_EF_from_scen_dict( + scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True + ) + + self.ef_instance = EF_instance + # set self.model_initialized flag to True to skip extensive form model + # creation using theta_est() + self.model_initialized = True + + # return initialized theta values + if len(thetavals) == 0: + # use appropriate theta_names member + theta_ref = self._return_theta_names() + for i, theta in enumerate(theta_ref): + thetavals[theta] = theta_init_vals[i]() + + return retval, thetavals, WorstStatus + + def _get_sample_list(self, samplesize, num_samples, replacement=True): + samplelist = list() + + scenario_numbers = list(range(len(self.callback_data))) + + if num_samples is None: + # This could get very large + for i, l in enumerate(combinations(scenario_numbers, samplesize)): + samplelist.append((i, np.sort(l))) + else: + for i in range(num_samples): + attempts = 0 + unique_samples = 0 # check for duplicates in each sample + duplicate = False # check for duplicates between samples + while (unique_samples <= len(self._return_theta_names())) and ( + not duplicate + ): + sample = np.random.choice( + scenario_numbers, samplesize, replace=replacement + ) + sample = np.sort(sample).tolist() + unique_samples = len(np.unique(sample)) + if sample in samplelist: + duplicate = True + + attempts += 1 + if attempts > num_samples: # arbitrary timeout limit + raise RuntimeError( + """Internal error: timeout constructing + a sample, the dim of theta may be too + close to the samplesize""" + ) + + samplelist.append((i, sample)) + + return samplelist + + def theta_est( + self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: string, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model for data reconciliation + calc_cov: boolean, optional + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) + cov_n: int, optional + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function + + Returns + ------- + objectiveval: float + The objective function value + thetavals: pd.Series + Estimated values for theta + variable values: pd.DataFrame + Variable values for each variable name in return_values (only for solver='ef_ipopt') + cov: pd.DataFrame + Covariance matrix of the fitted parameters (only for solver='ef_ipopt') + """ + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert isinstance(calc_cov, bool) + if calc_cov: + assert isinstance( + cov_n, int + ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert cov_n > len( + self._return_theta_names() + ), "The number of datapoints must be greater than the number of parameters to estimate" + + return self._Q_opt( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + def theta_est_bootstrap( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.callback_data) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta + + def theta_est_leaveNout( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.callback_data) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + lNo_s = list(set(range(len(self.callback_data))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + + def leaveNout_bootstrap_test( + self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None + ): + """ + Leave-N-out bootstrap test to compare theta values where N data points are + left out to a bootstrap analysis using the remaining data, + results indicate if theta is within a confidence region + determined by the bootstrap analysis + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Leave-N-out sample size. If lNo_samples=None, the maximum number + of combinations will be used + bootstrap_samples: int: + Bootstrap sample size + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + seed: int or None, optional + Random seed + + Returns + ---------- + List of tuples with one entry per lNo_sample: + + * The first item in each tuple is the list of N samples that are left + out. + * The second item in each tuple is a DataFrame of theta estimated using + the N samples. + * The third item in each tuple is a DataFrame containing results from + the bootstrap analysis using the remaining samples. + + For each DataFrame a column is added for each value of alpha which + indicates if the theta estimate is in (True) or out (False) of the + alpha region for a given distribution (based on the bootstrap results) + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(bootstrap_samples, int) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance(seed, (type(None), int)) + + if seed is not None: + np.random.seed(seed) + + data = self.callback_data.copy() + + global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) + + results = [] + for idx, sample in global_list: + # Reset callback_data to only include the sample + self.callback_data = [data[i] for i in sample] + + obj, theta = self.theta_est() + + # Reset callback_data to include all scenarios except the sample + self.callback_data = [data[i] for i in range(len(data)) if i not in sample] + + bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) + + training, test = self.confidence_region_test( + bootstrap_theta, + distribution=distribution, + alphas=alphas, + test_theta_values=theta, + ) + + results.append((sample, test, training)) + + # Reset callback_data (back to full data set) + self.callback_data = data + + return results + + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): + """ + Objective value for each theta + + Parameters + ---------- + theta_values: pd.DataFrame, columns=theta_names + Values of theta used to compute the objective + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + + Returns + ------- + obj_at_theta: pd.DataFrame + Objective value for each theta (infeasible solutions are + omitted). + """ + if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': + pass # skip assertion if model has no fitted parameters + else: + # create a local instance of the pyomo model to access model variables and parameters + model_temp = self._create_parmest_model(self.callback_data[0]) + model_theta_list = [] # list to store indexed and non-indexed parameters + # iterate over original theta_names + for theta_i in self.theta_names: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + # check if theta in theta_names are indexed + try: + # get component UID of Set over which theta is defined + set_cuid = ComponentUID(var_validate.index_set()) + # access and iterate over the Set to generate theta names as they appear + # in the pyomo model + set_validate = set_cuid.find_component_on(model_temp) + for s in set_validate: + self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" + # generate list of theta names + model_theta_list.append(self_theta_temp) + # if theta is not indexed, copy theta name to list as-is + except AttributeError: + self_theta_temp = repr(var_cuid) + model_theta_list.append(self_theta_temp) + except: + raise + # if self.theta_names is not the same as temp model_theta_list, + # create self.theta_names_updated + if set(self.theta_names) == set(model_theta_list) and len( + self.theta_names + ) == set(model_theta_list): + pass + else: + self.theta_names_updated = model_theta_list + + if theta_values is None: + all_thetas = {} # dictionary to store fitted variables + # use appropriate theta names member + theta_names = self._return_theta_names() + else: + assert isinstance(theta_values, pd.DataFrame) + # for parallel code we need to use lists and dicts in the loop + theta_names = theta_values.columns + # # check if theta_names are in model + for theta in list(theta_names): + theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + + assert theta_temp in [ + t.replace("'", "") for t in model_theta_list + ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + theta_temp, model_theta_list + ) + assert len(list(theta_names)) == len(model_theta_list) + + all_thetas = theta_values.to_dict('records') + + if all_thetas: + task_mgr = utils.ParallelTaskManager(len(all_thetas)) + local_thetas = task_mgr.global_to_local_data(all_thetas) + else: + if initialize_parmest_model: + task_mgr = utils.ParallelTaskManager( + 1 + ) # initialization performed using just 1 set of theta values + # walk over the mesh, return objective function + all_obj = list() + if len(all_thetas) > 0: + for Theta in local_thetas: + obj, thetvals, worststatus = self._Q_at_theta( + Theta, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(Theta.values()) + [obj]) + # DLW, Aug2018: should we also store the worst solver status? + else: + obj, thetvals, worststatus = self._Q_at_theta( + thetavals={}, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(thetvals.values()) + [obj]) + + global_all_obj = task_mgr.allgather_global_data(all_obj) + dfcols = list(theta_names) + ['obj'] + obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + return obj_at_theta + + def likelihood_ratio_test( + self, obj_at_theta, obj_value, alphas, return_thresholds=False + ): + r""" + Likelihood ratio test to identify theta values within a confidence + region using the :math:`\chi^2` distribution + + Parameters + ---------- + obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' + Objective values for each theta value (returned by + objective_at_theta) + obj_value: int or float + Objective value from parameter estimation using all data + alphas: list + List of alpha values to use in the chi2 test + return_thresholds: bool, optional + Return the threshold value for each alpha + + Returns + ------- + LR: pd.DataFrame + Objective values for each theta value along with True or False for + each alpha + thresholds: pd.Series + If return_threshold = True, the thresholds are also returned. + """ + assert isinstance(obj_at_theta, pd.DataFrame) + assert isinstance(obj_value, (int, float)) + assert isinstance(alphas, list) + assert isinstance(return_thresholds, bool) + + LR = obj_at_theta.copy() + S = len(self.callback_data) + thresholds = {} + for a in alphas: + chi2_val = scipy.stats.chi2.ppf(a, 2) + thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) + LR[a] = LR['obj'] < thresholds[a] + + thresholds = pd.Series(thresholds) + + if return_thresholds: + return LR, thresholds + else: + return LR + + def confidence_region_test( + self, theta_values, distribution, alphas, test_theta_values=None + ): + """ + Confidence region test to determine if theta values are within a + rectangular, multivariate normal, or Gaussian kernel density distribution + for a range of alpha values + + Parameters + ---------- + theta_values: pd.DataFrame, columns = theta_names + Theta values used to generate a confidence region + (generally returned by theta_est_bootstrap) + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional + Additional theta values that are compared to the confidence region + to determine if they are inside or outside. + + Returns + training_results: pd.DataFrame + Theta value used to generate the confidence region along with True + (inside) or False (outside) for each alpha + test_results: pd.DataFrame + If test_theta_values is not None, returns test theta value along + with True (inside) or False (outside) for each alpha + """ + assert isinstance(theta_values, pd.DataFrame) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance( + test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) + ) + + if isinstance(test_theta_values, (dict, pd.Series)): + test_theta_values = pd.Series(test_theta_values).to_frame().transpose() + + training_results = theta_values.copy() + + if test_theta_values is not None: + test_result = test_theta_values.copy() + + for a in alphas: + if distribution == 'Rect': + lb, ub = graphics.fit_rect_dist(theta_values, a) + training_results[a] = (theta_values > lb).all(axis=1) & ( + theta_values < ub + ).all(axis=1) + + if test_theta_values is not None: + # use upper and lower bound from the training set + test_result[a] = (test_theta_values > lb).all(axis=1) & ( + test_theta_values < ub + ).all(axis=1) + + elif distribution == 'MVN': + dist = graphics.fit_mvn_dist(theta_values) + Z = dist.pdf(theta_values) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values) + test_result[a] = Z >= score + + elif distribution == 'KDE': + dist = graphics.fit_kde_dist(theta_values) + Z = dist.pdf(theta_values.transpose()) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values.transpose()) + test_result[a] = Z >= score + + if test_theta_values is not None: + return training_results, test_result + else: + return training_results diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 76ea2f2ab81..434e15e7f31 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,7 +14,10 @@ import pyomo.environ as pyo -import pyomo.contrib.parmest.deprecated.scenariocreator as scen_deprecated +from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecation_warning + +DEPRECATION_VERSION = '6.7.0' import logging @@ -129,11 +132,12 @@ def __init__(self, pest, solvername): # is this a deprecated pest object? self.scen_deprecated = None if pest.pest_deprecated is not None: - logger.warning( + deprecation_warning( "Using a deprecated parmest object for scenario " - + "creator, please recreate object using experiment lists." + + "creator, please recreate object using experiment lists.", + version=DEPRECATION_VERSION, ) - self.scen_deprecated = scen_deprecated.ScenarioCreator( + self.scen_deprecated = ScenarioCreatorDeprecated( pest.pest_deprecated, solvername ) else: @@ -190,3 +194,65 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) + + +################################ +# deprecated functions/classes # +################################ + + +class ScenarioCreatorDeprecated(object): + """Create scenarios from parmest. + + Args: + pest (Estimator): the parmest object + solvername (str): name of the solver (e.g. "ipopt") + + """ + + def __init__(self, pest, solvername): + self.pest = pest + self.solvername = solvername + + def ScenariosFromExperiments(self, addtoSet): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + Returns: + a ScenarioSet + """ + + # assert isinstance(addtoSet, ScenarioSet) + + scenario_numbers = list(range(len(self.pest.callback_data))) + + prob = 1.0 / len(scenario_numbers) + for exp_num in scenario_numbers: + ##print("Experiment number=", exp_num) + model = self.pest._instance_creation_callback( + exp_num, self.pest.callback_data + ) + opt = pyo.SolverFactory(self.solvername) + results = opt.solve(model) # solves and updates model + ## pyo.check_termination_optimal(results) + ThetaVals = dict() + for theta in self.pest.theta_names: + tvar = eval('model.' + theta) + tval = pyo.value(tvar) + ##print(" theta, tval=", tvar, tval) + ThetaVals[theta] = tval + addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) + + def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + numtomake (int) : number of scenarios to create + """ + + # assert isinstance(addtoSet, ScenarioSet) + + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + addtoSet.append_bootstrap(bootstrap_thetas) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 15264a18989..9c65a31352f 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -232,16 +232,16 @@ def test_theta_est_cov(self): # Covariance matrix self.assertAlmostEqual( - cov['asymptote']['asymptote'], 6.30579403, places=2 + cov["asymptote"]["asymptote"], 6.30579403, places=2 ) # 6.22864 from paper self.assertAlmostEqual( - cov['asymptote']['rate_constant'], -0.4395341, places=2 + cov["asymptote"]["rate_constant"], -0.4395341, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov['rate_constant']['asymptote'], -0.4395341, places=2 + cov["rate_constant"]["asymptote"], -0.4395341, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov['rate_constant']['rate_constant'], 0.04124, places=2 + cov["rate_constant"]["rate_constant"], 0.04124, places=2 ) # 0.04124 from paper """ Why does the covariance matrix from parmest not match the paper? Parmest is @@ -427,8 +427,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data["hour"])]) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -506,8 +506,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data["hour"])]) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -575,9 +575,9 @@ def check_rooney_biegler_results(self, objval, cov): # get indices in covariance matrix cov_cols = cov.columns.to_list() - asymptote_index = [idx for idx, s in enumerate(cov_cols) if 'asymptote' in s][0] + asymptote_index = [idx for idx, s in enumerate(cov_cols) if "asymptote" in s][0] rate_constant_index = [ - idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s + idx for idx, s in enumerate(cov_cols) if "rate_constant" in s ][0] self.assertAlmostEqual(objval, 4.3317112, places=2) @@ -698,7 +698,7 @@ def setUp(self): solver_options = {"max_iter": 6000} self.pest = parmest.Estimator( - exp_list, obj_function='SSE', solver_options=solver_options + exp_list, obj_function="SSE", solver_options=solver_options ) def test_theta_est(self): @@ -1033,5 +1033,1031 @@ def test_theta_est_with_square_initialization_diagnostic_mode_true(self): self.pest.diagnostic_mode = False +########################### +# tests for deprecated UI # +########################### + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestRooneyBieglerDeprecated(unittest.TestCase): + def setUp(self): + + def rooney_biegler_model(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est(self): + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_bootstrap(self): + objval, thetavals = self.pest.theta_est() + + num_bootstraps = 10 + theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) + + num_samples = theta_est["samples"].apply(len) + self.assertTrue(len(theta_est.index), 10) + self.assertTrue(num_samples.equals(pd.Series([6] * 10))) + + del theta_est["samples"] + + # apply confidence region test + CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) + + self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) + self.assertTrue(CR[0.5].sum() == 5) + self.assertTrue(CR[0.75].sum() == 7) + self.assertTrue(CR[1.0].sum() == 10) # all true + + graphics.pairwise_plot(theta_est) + graphics.pairwise_plot(theta_est, thetavals) + graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_likelihood_ratio(self): + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) + + self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) + self.assertTrue(LR[0.8].sum() == 6) + self.assertTrue(LR[0.9].sum() == 10) + self.assertTrue(LR[1.0].sum() == 60) # all true + + graphics.pairwise_plot(LR, thetavals, 0.8) + + def test_leaveNout(self): + lNo_theta = self.pest.theta_est_leaveNout(1) + self.assertTrue(lNo_theta.shape == (6, 2)) + + results = self.pest.leaveNout_bootstrap_test( + 1, None, 3, "Rect", [0.5, 1.0], seed=5436 + ) + self.assertTrue(len(results) == 6) # 6 lNo samples + i = 1 + samples = results[i][0] # list of N samples that are left out + lno_theta = results[i][1] + bootstrap_theta = results[i][2] + self.assertTrue(samples == [1]) # sample 1 was left out + self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 + self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) + self.assertTrue(lno_theta[1.0].sum() == 1) # all true + self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 + self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true + + def test_diagnostic_mode(self): + self.pest.diagnostic_mode = True + + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + self.pest.diagnostic_mode = False + + @unittest.skip("Presently having trouble with mpiexec on appveyor") + def test_parallel_parmest(self): + """use mpiexec and mpi4py""" + p = str(parmestbase.__path__) + l = p.find("'") + r = p.find("'", l + 1) + parmestpath = p[l + 1 : r] + rbpath = ( + parmestpath + + os.sep + + "examples" + + os.sep + + "rooney_biegler" + + os.sep + + "rooney_biegler_parmest.py" + ) + rbpath = os.path.abspath(rbpath) # paranoia strikes deep... + rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] + if sys.version_info >= (3, 5): + ret = subprocess.run(rlist) + retcode = ret.returncode + else: + retcode = subprocess.call(rlist) + assert retcode == 0 + + @unittest.skip("Most folks don't have k_aug installed") + def test_theta_k_aug_for_Hessian(self): + # this will fail if k_aug is not installed + objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") + self.assertAlmostEqual(objval, 4.4675, places=2) + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_theta_est_cov(self): + objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + # Covariance matrix + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper + + """ Why does the covariance matrix from parmest not match the paper? Parmest is + calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely + employed the first order approximation common for nonlinear regression. The paper + values were verified with Scipy, which uses the same first order approximation. + The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in + "Nonlinear Parameter Estimation", Y. Bard, 1974. + """ + + def test_cov_scipy_least_squares_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + def model(theta, t): + """ + Model to be fitted y = model(theta, t) + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + + Returns: + y: model predictions [need to check paper for units] + """ + asymptote = theta[0] + rate_constant = theta[1] + + return asymptote * (1 - np.exp(-rate_constant * t)) + + def residual(theta, t, y): + """ + Calculate residuals + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + y: dependent variable [?] + """ + return y - model(theta, t) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + ## solve with optimize.least_squares + sol = scipy.optimize.least_squares( + residual, theta_guess, method="trf", args=(t, y), verbose=2 + ) + theta_hat = sol.x + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + # calculate residuals + r = residual(theta_hat, t, y) + + # calculate variance of the residuals + # -2 because there are 2 fitted parameters + sigre = np.matmul(r.T, r / (len(y) - 2)) + + # approximate covariance + # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 + cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + def test_cov_scipy_curve_fit_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + ## solve with optimize.curve_fit + def model(t, asymptote, rate_constant): + return asymptote * (1 - np.exp(-rate_constant * t)) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestModelVariantsDeprecated(unittest.TestCase): + def setUp(self): + self.data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + def rooney_biegler_params(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Param(initialize=15, mutable=True) + model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_params(data): + model = pyo.ConcreteModel() + + model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Param( + model.param_names, + initialize={"asymptote": 15, "rate_constant": 0.5}, + mutable=True, + ) + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_vars(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.asymptote.fixed = True # parmest will unfix theta variables + model.rate_constant.fixed = True + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_vars(data): + model = pyo.ConcreteModel() + + model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Var( + model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} + ) + model.theta["asymptote"].fixed = ( + True # parmest will unfix theta variables, even when they are indexed + ) + model.theta["rate_constant"].fixed = True + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + self.objective_function = SSE + + theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T + theta_vals_index = pd.DataFrame( + [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] + ).T + + self.input = { + "param": { + "model": rooney_biegler_params, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "param_index": { + "model": rooney_biegler_indexed_params, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars": { + "model": rooney_biegler_vars, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "vars_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars_quoted_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, + }, + "vars_str_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, + }, + } + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_parmest_basics(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_initialize_parmest_model_option(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesignDeprecated(unittest.TestCase): + def setUp(self): + + def reactor_design_model(data): + # Create the concrete model + model = pyo.ConcreteModel() + + # Rate constants + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = pyo.Param( + initialize=float(data["caf"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.caf = pyo.Param( + initialize=float(data.iloc[0]["caf"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = pyo.Param( + initialize=float(data["sv"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.sv = pyo.Param( + initialize=float(data.iloc[0]["sv"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=( + 0 + == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb + ) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + solver_options = {"max_iter": 6000} + + self.pest = parmest.Estimator( + reactor_design_model, data, theta_names, SSE, solver_options=solver_options + ) + + def test_theta_est(self): + # used in data reconciliation + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) + self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) + self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) + + def test_return_values(self): + objval, thetavals, data_rec = self.pest.theta_est( + return_values=["ca", "cb", "cc", "cd", "caf"] + ) + self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign_DAE_Deprecated(unittest.TestCase): + # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html + + def setUp(self): + def ABC_model(data): + ca_meas = data["ca"] + cb_meas = data["cb"] + cc_meas = data["cc"] + + if isinstance(data, pd.DataFrame): + meas_t = data.index # time index + else: # dictionary + meas_t = list(ca_meas.keys()) # nested dictionary + + ca0 = 1.0 + cb0 = 0.0 + cc0 = 0.0 + + m = pyo.ConcreteModel() + + m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) + m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) + + m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) + + # initialization and bounds + m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) + m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) + m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) + + m.dca = dae.DerivativeVar(m.ca, wrt=m.time) + m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) + m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) + + def _dcarate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dca[t] == -m.k1 * m.ca[t] + + m.dcarate = pyo.Constraint(m.time, rule=_dcarate) + + def _dcbrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] + + m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) + + def _dccrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcc[t] == m.k2 * m.cb[t] + + m.dccrate = pyo.Constraint(m.time, rule=_dccrate) + + def ComputeFirstStageCost_rule(m): + return 0 + + m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) + + def ComputeSecondStageCost_rule(m): + return sum( + (m.ca[t] - ca_meas[t]) ** 2 + + (m.cb[t] - cb_meas[t]) ** 2 + + (m.cc[t] - cc_meas[t]) ** 2 + for t in meas_t + ) + + m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = pyo.Objective( + rule=total_cost_rule, sense=pyo.minimize + ) + + disc = pyo.TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=2) + + return m + + # This example tests data formatted in 3 ways + # Each format holds 1 scenario + # 1. dataframe with time index + # 2. nested dictionary {ca: {t, val pairs}, ... } + data = [ + [0.000, 0.957, -0.031, -0.015], + [0.263, 0.557, 0.330, 0.044], + [0.526, 0.342, 0.512, 0.156], + [0.789, 0.224, 0.499, 0.310], + [1.053, 0.123, 0.428, 0.454], + [1.316, 0.079, 0.396, 0.556], + [1.579, 0.035, 0.303, 0.651], + [1.842, 0.029, 0.287, 0.658], + [2.105, 0.025, 0.221, 0.750], + [2.368, 0.017, 0.148, 0.854], + [2.632, -0.002, 0.182, 0.845], + [2.895, 0.009, 0.116, 0.893], + [3.158, -0.023, 0.079, 0.942], + [3.421, 0.006, 0.078, 0.899], + [3.684, 0.016, 0.059, 0.942], + [3.947, 0.014, 0.036, 0.991], + [4.211, -0.009, 0.014, 0.988], + [4.474, -0.030, 0.036, 0.941], + [4.737, 0.004, 0.036, 0.971], + [5.000, -0.024, 0.028, 0.985], + ] + data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) + data_df = data.set_index("t") + data_dict = { + "ca": {k: v for (k, v) in zip(data.t, data.ca)}, + "cb": {k: v for (k, v) in zip(data.t, data.cb)}, + "cc": {k: v for (k, v) in zip(data.t, data.cc)}, + } + + theta_names = ["k1", "k2"] + + self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) + self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) + + # Estimator object with multiple scenarios + self.pest_df_multiple = parmest.Estimator( + ABC_model, [data_df, data_df], theta_names + ) + self.pest_dict_multiple = parmest.Estimator( + ABC_model, [data_dict, data_dict], theta_names + ) + + # Create an instance of the model + self.m_df = ABC_model(data_df) + self.m_dict = ABC_model(data_dict) + + def test_dataformats(self): + obj1, theta1 = self.pest_df.theta_est() + obj2, theta2 = self.pest_dict.theta_est() + + self.assertAlmostEqual(obj1, obj2, places=6) + self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) + self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) + + def test_return_continuous_set(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) + obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) + self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) + + def test_return_continuous_set_multiple_datasets(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( + return_values=["time"] + ) + obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( + return_values=["time"] + ) + self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + + def test_covariance(self): + from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + inv_reduced_hessian_barrier, + ) + + # Number of datapoints. + # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # In this example, this is the number of data points in data_df, but that's + # only because the data is indexed by time and contains no additional information. + n = 60 + + # Compute covariance using parmest + obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # Compute covariance using interior_point + vars_list = [self.m_df.k1, self.m_df.k2] + solve_result, inv_red_hes = inv_reduced_hessian_barrier( + self.m_df, independent_variables=vars_list, tee=True + ) + l = len(vars_list) + cov_interior_point = 2 * obj / (n - l) * inv_red_hes + cov_interior_point = pd.DataFrame( + cov_interior_point, ["k1", "k2"], ["k1", "k2"] + ) + + cov_diff = (cov - cov_interior_point).abs().sum().sum() + + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) + self.assertAlmostEqual(cov_diff, 0, places=6) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSquareInitialization_RooneyBiegler_Deprecated(unittest.TestCase): + def setUp(self): + + def rooney_biegler_model_with_constraint(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.response_function = pyo.Var(data.hour, initialize=0.0) + + # changed from expression to constraint + def response_rule(m, h): + return m.response_function[h] == m.asymptote * ( + 1 - pyo.exp(-m.rate_constant * h) + ) + + model.response_function_constraint = pyo.Constraint( + data.hour, rule=response_rule + ) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model_with_constraint, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est_with_square_initialization(self): + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_and_custom_init_theta(self): + theta_vals_init = pd.DataFrame( + data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] + ) + obj_init = self.pest.objective_at_theta( + theta_values=theta_vals_init, initialize_parmest_model=True + ) + objval, thetavals = self.pest.theta_est() + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_diagnostic_mode_true(self): + self.pest.diagnostic_mode = True + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + self.pest.diagnostic_mode = False + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index 1f8ccdb20fe..af755e34b67 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -138,5 +138,453 @@ def test_semibatch_bootstrap(self): self.assertAlmostEqual(tval, 20.64, places=1) +########################### +# tests for deprecated UI # +########################### + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioReactorDesignDeprecated(unittest.TestCase): + def setUp(self): + + def reactor_design_model(data): + # Create the concrete model + model = pyo.ConcreteModel() + + # Rate constants + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = pyo.Param( + initialize=float(data["caf"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.caf = pyo.Param( + initialize=float(data.iloc[0]["caf"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = pyo.Param( + initialize=float(data["sv"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.sv = pyo.Param( + initialize=float(data.iloc[0]["sv"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=( + 0 + == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb + ) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + def test_scen_from_exps(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv("delme_exp_csv.csv") + df = pd.read_csv("delme_exp_csv.csv") + os.remove("delme_exp_csv.csv") + # March '20: all reactor_design experiments have the same theta values! + k1val = df.loc[5].at["k1"] + self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) + tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) + + @unittest.skipIf(not uuid_available, "The uuid module is not available") + def test_no_csv_if_empty(self): + # low level test of scenario sets + # verify that nothing is written, but no errors with empty set + + emptyset = sc.ScenarioSet("empty") + tfile = uuid.uuid4().hex + ".csv" + emptyset.write_csv(tfile) + self.assertFalse( + os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" + ) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioSemibatchDeprecated(unittest.TestCase): + def setUp(self): + + import json + from pyomo.environ import ( + ConcreteModel, + Set, + Param, + Var, + Constraint, + ConstraintList, + Expression, + Objective, + TransformationFactory, + SolverFactory, + exp, + minimize, + ) + from pyomo.dae import ContinuousSet, DerivativeVar + + def generate_model(data): + # if data is a file name, then load file first + if isinstance(data, str): + file_name = data + try: + with open(file_name, "r") as infile: + data = json.load(infile) + except: + raise RuntimeError(f"Could not read {file_name} as json") + + # unpack and fix the data + cameastemp = data["Ca_meas"] + cbmeastemp = data["Cb_meas"] + ccmeastemp = data["Cc_meas"] + trmeastemp = data["Tr_meas"] + + cameas = {} + cbmeas = {} + ccmeas = {} + trmeas = {} + for i in cameastemp.keys(): + cameas[float(i)] = cameastemp[i] + cbmeas[float(i)] = cbmeastemp[i] + ccmeas[float(i)] = ccmeastemp[i] + trmeas[float(i)] = trmeastemp[i] + + m = ConcreteModel() + + # + # Measurement Data + # + m.measT = Set(initialize=sorted(cameas.keys())) + m.Ca_meas = Param(m.measT, initialize=cameas) + m.Cb_meas = Param(m.measT, initialize=cbmeas) + m.Cc_meas = Param(m.measT, initialize=ccmeas) + m.Tr_meas = Param(m.measT, initialize=trmeas) + + # + # Parameters for semi-batch reactor model + # + m.R = Param(initialize=8.314) # kJ/kmol/K + m.Mwa = Param(initialize=50.0) # kg/kmol + m.rhor = Param(initialize=1000.0) # kg/m^3 + m.cpr = Param(initialize=3.9) # kJ/kg/K + m.Tf = Param(initialize=300) # K + m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol + m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol + m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K + m.alphac = Param(initialize=0.7) # kJ/s/m^2/K + m.Aj = Param(initialize=5.0) # m^2 + m.Ac = Param(initialize=3.0) # m^2 + m.Vj = Param(initialize=0.9) # m^3 + m.Vc = Param(initialize=0.07) # m^3 + m.rhow = Param(initialize=700.0) # kg/m^3 + m.cpw = Param(initialize=3.1) # kJ/kg/K + m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) + m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) + m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) + m.Tr0 = Param(initialize=300.0) # K + m.Vr0 = Param(initialize=1.0) # m^3 + + m.time = ContinuousSet( + bounds=(0, 21600), initialize=m.measT + ) # Time in seconds + + # + # Control Inputs + # + def _initTc(m, t): + if t < 10800: + return data["Tc1"] + else: + return data["Tc2"] + + m.Tc = Param( + m.time, initialize=_initTc, default=_initTc + ) # bounds= (288,432) Cooling coil temp, control input + + def _initFa(m, t): + if t < 10800: + return data["Fa1"] + else: + return data["Fa2"] + + m.Fa = Param( + m.time, initialize=_initFa, default=_initFa + ) # bounds=(0,0.05) Inlet flow rate, control input + + # + # Parameters being estimated + # + m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 + m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 + m.E1 = Var( + initialize=27000.0, bounds=(25000, 40000) + ) # kJ/kmol Actual: 30000 + m.E2 = Var( + initialize=45000.0, bounds=(35000, 50000) + ) # kJ/kmol Actual: 40000 + # m.E1.fix(30000) + # m.E2.fix(40000) + + # + # Time dependent variables + # + m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) + m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) + m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) + m.Vr = Var(m.time, initialize=m.Vr0) + m.Tr = Var(m.time, initialize=m.Tr0) + m.Tj = Var( + m.time, initialize=310.0, bounds=(288, None) + ) # Cooling jacket temp, follows coil temp until failure + + # + # Derivatives in the model + # + m.dCa = DerivativeVar(m.Ca) + m.dCb = DerivativeVar(m.Cb) + m.dCc = DerivativeVar(m.Cc) + m.dVr = DerivativeVar(m.Vr) + m.dTr = DerivativeVar(m.Tr) + + # + # Differential Equations in the model + # + + def _dCacon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCa[t] + == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + ) + + m.dCacon = Constraint(m.time, rule=_dCacon) + + def _dCbcon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCb[t] + == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + ) + + m.dCbcon = Constraint(m.time, rule=_dCbcon) + + def _dCccon(m, t): + if t == 0: + return Constraint.Skip + return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + + m.dCccon = Constraint(m.time, rule=_dCccon) + + def _dVrcon(m, t): + if t == 0: + return Constraint.Skip + return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor + + m.dVrcon = Constraint(m.time, rule=_dVrcon) + + def _dTrcon(m, t): + if t == 0: + return Constraint.Skip + return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[ + t + ] * (m.Tf - m.Tr[t]) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[ + t + ] * m.deltaH1 - m.k2 * exp( + -m.E2 / (m.R * m.Tr[t]) + ) * m.Cb[ + t + ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( + m.Tj[t] - m.Tr[t] + ) + m.alphac * m.Ac / m.Vr0 * ( + m.Tc[t] - m.Tr[t] + ) + + m.dTrcon = Constraint(m.time, rule=_dTrcon) + + def _singlecooling(m, t): + return m.Tc[t] == m.Tj[t] + + m.singlecooling = Constraint(m.time, rule=_singlecooling) + + # Initial Conditions + def _initcon(m): + yield m.Ca[m.time.first()] == m.Ca0 + yield m.Cb[m.time.first()] == m.Cb0 + yield m.Cc[m.time.first()] == m.Cc0 + yield m.Vr[m.time.first()] == m.Vr0 + yield m.Tr[m.time.first()] == m.Tr0 + + m.initcon = ConstraintList(rule=_initcon) + + # + # Stage-specific cost computations + # + def ComputeFirstStageCost_rule(model): + return 0 + + m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + def MissingMeasurements(m): + if data["experiment"] == 1: + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + elif data["experiment"] == 2: + return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) + else: + return sum( + (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + m.SecondStageCost = Expression(rule=MissingMeasurements) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + # Discretize model + disc = TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=4) + return m + + # Vars to estimate in parmest + theta_names = ["k1", "k2", "E1", "E2"] + + self.fbase = os.path.join(testdir, "..", "examples", "semibatch") + # Data, list of dictionaries + data = [] + for exp_num in range(10): + fname = "exp" + str(exp_num + 1) + ".out" + fullname = os.path.join(self.fbase, fname) + with open(fullname, "r") as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for the sum of squared error that will be used in parameter estimation + + self.pest = parmest.Estimator(generate_model, data, theta_names) + + def test_semibatch_bootstrap(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + bootscens = sc.ScenarioSet("Bootstrap") + numtomake = 2 + scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) + tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 20.64, places=1) + + if __name__ == "__main__": unittest.main() From 41d8197bbc2627aa6742d0358e16ade0a93a4747 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 16:23:41 -0700 Subject: [PATCH 0475/1178] Support config domains with either method or attribute domain_name --- pyomo/common/config.py | 6 +++++- pyomo/common/tests/test_config.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 238bdd78e9d..f9c3a725bb8 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1134,7 +1134,11 @@ def _domain_name(domain): if domain is None: return "" elif hasattr(domain, 'domain_name'): - return domain.domain_name() + dn = domain.domain_name + if hasattr(dn, '__call__'): + return dn() + else: + return dn elif domain.__class__ is type: return domain.__name__ elif inspect.isfunction(domain): diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 0bbed43423d..12657481764 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -3265,6 +3265,41 @@ def __init__( OUT.getvalue().replace('null', 'None'), ) + def test_domain_name(self): + cfg = ConfigDict() + + cfg.declare('none', ConfigValue()) + self.assertEqual(cfg.get('none').domain_name(), '') + + def fcn(val): + return val + + cfg.declare('fcn', ConfigValue(domain=fcn)) + self.assertEqual(cfg.get('fcn').domain_name(), 'fcn') + + fcn.domain_name = 'custom fcn' + self.assertEqual(cfg.get('fcn').domain_name(), 'custom fcn') + + class functor: + def __call__(self, val): + return val + + cfg.declare('functor', ConfigValue(domain=functor())) + self.assertEqual(cfg.get('functor').domain_name(), 'functor') + + class cfunctor: + def __call__(self, val): + return val + + def domain_name(self): + return 'custom functor' + + cfg.declare('cfunctor', ConfigValue(domain=cfunctor())) + self.assertEqual(cfg.get('cfunctor').domain_name(), 'custom functor') + + cfg.declare('type', ConfigValue(domain=int)) + self.assertEqual(cfg.get('type').domain_name(), 'int') + if __name__ == "__main__": unittest.main() From df19b6bf869f94b792aa30733edfdff68b3da7ad Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jul 2023 11:36:32 -0600 Subject: [PATCH 0476/1178] add initial work on nested inner repn pw to gdp transformation. identify variables mode does not work, gives infeasible models --- .../tests/test_nested_inner_repn_gdp.py | 36 ++++ .../piecewise/transform/nested_inner_repn.py | 171 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py create mode 100644 pyomo/contrib/piecewise/transform/nested_inner_repn.py diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py new file mode 100644 index 00000000000..48357c828df --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var + +from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.nested_inner_repn_gdp' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py new file mode 100644 index 00000000000..b25ca3981a8 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -0,0 +1,171 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components + +@TransformationFactory.register( + 'contrib.piecewise.nested_inner_repn_gdp', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a nested + GDP to determine which polytope a point is in, then representing it as a + convex combination of extreme points, with multipliers "local" to that + particular polytope, i.e., not shared with neighbors. This method of + logarithmically formulating the piecewise linear function imposes no + restrictions on the family of polytopes. We rely on the identification of + variables to make this logarithmic in the number of binaries. This method + is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_nested_inner_repn' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = True + identify_vars = True + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # these copy-pasted lines (from inner_representation_gdp) seem useful + # adding some of this stuff to self so I don't have to pass it around + self.pw_linear_func = pw_linear_func + # map number -> list of Disjuncts which contain Disjunctions at that level + self.disjunct_levels = {} + self.dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + if self.DEBUG: + print(f"dimension is {self.dimension}") + + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + + # Widen bounds as determined when setting up the disjunction + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) + + if self.DEBUG: + print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + + if identify_vars: + if self.DEBUG: + print("Now identifying variables") + for i in self.disjunct_levels.keys(): + print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") + transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) + transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) + for k in self.disjunct_levels.keys(): + disj_0 = self.disjunct_levels[k][0] + for i, disj in enumerate(self.disjunct_levels[k][1:]): + transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var + transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var + return substitute_var + + # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up + # the stack, since the whole point is that we'll only go logarithmically + # many calls deep. + def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + size = len(choices) + if self.DEBUG: + print(f"calling _get_disjunction with size={size}") + # Our base cases will be 3 and 2, since it would be silly to construct + # a Disjunction containing only one Disjunct. We can ensure that size + # is never 1 unless it was only passsed a single choice from the start, + # which we can handle before calling. + if size > 3: + half = size // 2 # (integer divide) + # This tree will be slightly heavier on the right side + choices_l = choices[:half] + choices_r = choices[half:] + # Is this valid Pyomo? + @parent_block.Disjunct() + def d_l(b): + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_l) + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 3: + # Let's stay heavier on the right side for consistency. So the left + # Disjunct will be the one to contain constraints, rather than a + # Disjunction + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 2: + # In this case both sides are regular Disjuncts + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + simplex, linear_func = choices[1] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + else: + raise DeveloperError("Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py.") + + def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): + # Define the lambdas sparsely like in the version I'm copying, + # only the first few will participate in constraints + b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + # Get the extreme points to add up + extreme_pts = [] + for idx in simplex: + extreme_pts.append(self.pw_linear_func._points[idx]) + # Constrain sum(lambda_i) = 1 + b.convex_combo = Constraint( + expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 + ) + linear_func_expr = linear_func(*pw_expr.args) + # Make the substitute Var equal the PWLE + b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + # Widen the variable bounds to those of this linear func expression + (lb, ub) = compute_bounds_on_expr(linear_func_expr) + if lb is not None and lb < self.substitute_var_lb: + self.substitute_var_lb = lb + if ub is not None and ub > self.substitute_var_ub: + self.substitute_var_ub = ub + # Constrain x = \sum \lambda_i v_i + @b.Constraint(range(self.dimension)) + def linear_combo(d, i): + return pw_expr.args[i] == sum( + d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) + ) + # Mark the lambdas as local in order to prevent disagreggating multiple + # times in the hull transformation + b.LocalVars = Suffix(direction=Suffix.LOCAL) + b.LocalVars[b] = [v for v in b.lambdas.values()] From 3e600ce5d32262053d46f1328a9008c6719cfe5b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 1 Aug 2023 12:48:46 -0600 Subject: [PATCH 0477/1178] wip: working on some other pw linear representations --- .../transform/disagreggated_logarithmic.py | 102 ++++++++++++++++++ .../piecewise/transform/nested_inner_repn.py | 7 +- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py new file mode 100644 index 00000000000..fceb02d4d8c --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -0,0 +1,102 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components +from math import ceil, log2 + +@TransformationFactory.register( + 'contrib.piecewise.disaggregated_logarithmic', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_disaggregated_log' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + simplex_indices = range(num_simplices) + # Assumption: the simplices are really simplices and all have the same number of points + simplex_point_indices = range(len(simplices[0])) + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + log_dimension = ceil(log2(num_simplices)) + binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + + # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors + B = {} + for i, p in enumerate(simplices): + B[id(p)] = self._get_binary_vector(i, log_dimension) + + # The lambdas \lambda_{P,v} + lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) + transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + + # The branching rules, establishing using the binaries that only one simplex's lambdas + # may be nonzero + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_1(b, l): + return ( + sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + ) + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_2(b, l): + return ( + sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + ) + + #for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i) + @transBlock.Constraint(range(dimension)) + def x_constraint(b, i): + return sum([stuff] for ) + + + #linear_func_expr = linear_func(*pw_expr.args) + ## Make the substitute Var equal the PWLE + #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + + # Not a gray code, just a regular binary representation + # TODO this is probably not optimal, test the gray codes too + def _get_binary_vector(self, num, length): + if ceil(log2(num)) > length: + raise DeveloperError("Invalid input in _get_binary_vector") + # Use python's string formatting instead of bothering with modular + # arithmetic. May be slow. + return (int(x) for x in format(num, f'0{length}b')) + + # Return {P \in \mathcal{P} | B(P)_l = 0} + def _P_0(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} + def _P_plus(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index b25ca3981a8..fc5761de434 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -30,8 +30,8 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = True - identify_vars = True + self.DEBUG = False + identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -66,6 +66,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + # NOTE - This functionality does not work. Even when we can choose the indicator + # variables, it seems that infeasibilities will always be generated. We may need + # to just directly transform to mip :( if identify_vars: if self.DEBUG: print("Now identifying variables") From c86220450129db5ac1f12020aae3d5e620b90014 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 16:34:57 -0400 Subject: [PATCH 0478/1178] properly handle one-simplex case instead of ignoring --- .../piecewise/transform/nested_inner_repn.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index fc5761de434..1e86a1406b4 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -54,8 +54,17 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"dimension is {self.dimension}") - # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + # If there was only one choice, don't bother making a disjunction, just + # use the linear function directly (but still use the substitute_var for + # consistency). + if len(choices) == 1: + (_, linear_func) = choices[0] # simplex isn't important in this case + linear_func_expr = linear_func(*pw_expr.args) + transBlock.set_substitute = Constraint(expr=substitute_var == linear_func_expr) + (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) + else: + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): From 8b1997fd1a3604581937c50eb643d915b5b74bbb Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:03:43 -0400 Subject: [PATCH 0479/1178] nested inner repn: remove non-working variable identification code --- .../piecewise/transform/nested_inner_repn.py | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 1e86a1406b4..aaa0e03c79b 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -2,27 +2,25 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction +from pyomo.gdp import Disjunction from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components @TransformationFactory.register( 'contrib.piecewise.nested_inner_repn_gdp', - doc="TODO document", + doc="TODO document", # TODO ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): """ - Represent a piecewise linear function "logarithmically" by using a nested - GDP to determine which polytope a point is in, then representing it as a - convex combination of extreme points, with multipliers "local" to that - particular polytope, i.e., not shared with neighbors. This method of - logarithmically formulating the piecewise linear function imposes no - restrictions on the family of polytopes. We rely on the identification of - variables to make this logarithmic in the number of binaries. This method - is due to Vielma et al., 2010. + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic + formulation of Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() _transformation_name = 'pw_linear_nested_inner_repn' @@ -31,7 +29,6 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False - identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -41,8 +38,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # these copy-pasted lines (from inner_representation_gdp) seem useful # adding some of this stuff to self so I don't have to pass it around self.pw_linear_func = pw_linear_func - # map number -> list of Disjuncts which contain Disjunctions at that level - self.disjunct_levels = {} self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) @@ -64,7 +59,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) else: # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): @@ -75,21 +70,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") - # NOTE - This functionality does not work. Even when we can choose the indicator - # variables, it seems that infeasibilities will always be generated. We may need - # to just directly transform to mip :( - if identify_vars: - if self.DEBUG: - print("Now identifying variables") - for i in self.disjunct_levels.keys(): - print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") - transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) - transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) - for k in self.disjunct_levels.keys(): - disj_0 = self.disjunct_levels[k][0] - for i, disj in enumerate(self.disjunct_levels[k][1:]): - transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var - transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -111,14 +91,10 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): # Is this valid Pyomo? @parent_block.Disjunct() def d_l(b): - b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_l) - self.disjunct_levels[level].append(parent_block.d_r) + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 3: # Let's stay heavier on the right side for consistency. So the left @@ -131,9 +107,6 @@ def d_l(b): @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_r) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From b1cc43403dfe50beeca3023809cfcd0e1b23d3fe Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:13:15 -0400 Subject: [PATCH 0480/1178] fix errors --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index aaa0e03c79b..6c551818c84 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -75,7 +75,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + def _get_disjunction(self, choices, parent_block, pw_expr, root_block): size = len(choices) if self.DEBUG: print(f"calling _get_disjunction with size={size}") @@ -106,7 +106,7 @@ def d_l(b): self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From 601abcbaf7b4d7db5cdf66c43a1f5199a3226a5d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 00:38:54 -0400 Subject: [PATCH 0481/1178] disaggregated logarithmic reworking --- .../transform/disagreggated_logarithmic.py | 186 +++++++++++++----- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index fceb02d4d8c..e0b6d75d0e4 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -2,7 +2,7 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -10,93 +10,191 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 + @TransformationFactory.register( - 'contrib.piecewise.disaggregated_logarithmic', - doc="TODO document", -) -class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): - """ + "contrib.piecewise.disaggregated_logarithmic", + doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This method of logarithmically formulating the piecewise linear function imposes no restrictions on the family of polytopes. This method is due to Vielma et al., 2010. + """, +) +class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_disaggregated_log' - + _transformation_name = "pw_linear_disaggregated_log" + # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any) + # is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] + # Dimensionality of the PWLF dimension = pw_expr.nargs() + print(f"DIMENSIOn={dimension}") + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') + # Bounds for the substitute_var that we will tighten + self.substitute_var_lb = float("inf") + self.substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too simplices = pw_linear_func._simplices num_simplices = len(simplices) - simplex_indices = range(num_simplices) - # Assumption: the simplices are really simplices and all have the same number of points - simplex_point_indices = range(len(simplices[0])) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # Assumption: the simplices are really simplices and all have the same number of points, + # which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + + # Enumeration of simplices, map from simplex number to simplex object + self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + # Inverse of previous enumeration + self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} + + # List of tuples of simplices with their linear function + simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + print("a") + print(f"Num_simplices: {num_simplices}") log_dimension = ceil(log2(num_simplices)) - binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors + # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is valid. B = {} - for i, p in enumerate(simplices): - B[id(p)] = self._get_binary_vector(i, log_dimension) - - # The lambdas \lambda_{P,v} - lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) - transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + print(f"after construction, B = {B}") + + print("b") + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + print("b1") + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + print("c") # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero - @transBlock.Constraint(range(log_dimension)) + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): + print("entering constraint generator") + print(f"thing={self._P_plus(B, l, simplices)}") + print("returning") return ( - sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_plus(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= binaries[l] ) - @transBlock.Constraint(range(log_dimension)) + + print("c1") + + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( - sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_0(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= 1 - binaries[l] ) - - #for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i) - @transBlock.Constraint(range(dimension)) + + print("d") + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - return sum([stuff] for ) + print(f"simplices are {[P for P in simplices]}") + print(f"points are {pw_linear_func._points}") + print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") + print(f"i={i}") + + return pw_expr.args[i] == sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * pw_linear_func._points[P[v]][i] + for P in simplices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + for P, linear_func in simplices_and_lin_funcs: + print(f"P, linear_func = {P}, {linear_func}") + for v in transBlock.simplex_point_indices: + print(f" v={v}") + print(f" pt={pw_linear_func._points[P[v]]}") + print( + f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + ) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * linear_func(*pw_linear_func._points[P[v]]) + for v in transBlock.simplex_point_indices + ) + for (P, linear_func) in simplices_and_lin_funcs + ) + ) + + print("f") + return substitute_var - #linear_func_expr = linear_func(*pw_expr.args) - ## Make the substitute Var equal the PWLE - #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) - # Not a gray code, just a regular binary representation # TODO this is probably not optimal, test the gray codes too def _get_binary_vector(self, num, length): - if ceil(log2(num)) > length: + if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") - # Use python's string formatting instead of bothering with modular + # Hack: use python's string formatting instead of bothering with modular # arithmetic. May be slow. - return (int(x) for x in format(num, f'0{length}b')) + return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 0] + def _P_0(self, B, l, simplices): + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file + def _P_plus(self, B, l, simplices): + print(f"p plus: B={B}, l={l}, simplices={simplices}") + for p in simplices: + print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") + print( + f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" + ) + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From ee616bf226d4d23b7b4ad7dfbacfcc94bfb4a715 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 01:34:14 -0400 Subject: [PATCH 0482/1178] remove printf debugging --- .../transform/disagreggated_logarithmic.py | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e0b6d75d0e4..00fb1546412 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -34,7 +34,6 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any). This is where we will put our new components. @@ -44,14 +43,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Dimensionality of the PWLF dimension = pw_expr.nargs() - print(f"DIMENSIOn={dimension}") transBlock.dimension_indices = RangeSet(0, dimension - 1) # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - # Bounds for the substitute_var that we will tighten + # Bounds for the substitute_var that we will widen self.substitute_var_lb = float("inf") self.substitute_var_ub = -float("inf") @@ -71,26 +69,35 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # List of tuples of simplices with their linear function simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - print("a") - print(f"Num_simplices: {num_simplices}") + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in simplices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[P[v]]) + if val < self.substitute_var_lb: + self.substitute_var_lb = val + if val > self.substitute_var_ub: + self.substitute_var_ub = val + # Now set those bounds + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function - # is valid. + # is enough here. B = {} for i in transBlock.simplex_indices: # map index(P) -> corresponding vector in {0, 1}^n B[i] = self._get_binary_vector(i, log_dimension) - print(f"after construction, B = {B}") - print("b") # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) - print("b1") # Sum of all lambdas is one (6b) transBlock.convex_combo = Constraint( @@ -102,15 +109,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc == 1 ) - print("c") - # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): - print("entering constraint generator") - print(f"thing={self._P_plus(B, l, simplices)}") - print("returning") return ( sum( transBlock.lambdas[self.simplex_to_idx[P], v] @@ -120,8 +122,6 @@ def simplex_choice_1(b, l): <= binaries[l] ) - print("c1") - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( @@ -133,18 +133,10 @@ def simplex_choice_2(b, l): <= 1 - binaries[l] ) - print("d") - # for i, (simplex, pwlf) in enumerate(choices): # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - - print(f"simplices are {[P for P in simplices]}") - print(f"points are {pw_linear_func._points}") - print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") - print(f"i={i}") - return pw_expr.args[i] == sum( transBlock.lambdas[self.simplex_to_idx[P], v] * pw_linear_func._points[P[v]][i] @@ -153,14 +145,14 @@ def x_constraint(b, i): ) # Make the substitute Var equal the PWLE (6a.2) - for P, linear_func in simplices_and_lin_funcs: - print(f"P, linear_func = {P}, {linear_func}") - for v in transBlock.simplex_point_indices: - print(f" v={v}") - print(f" pt={pw_linear_func._points[P[v]]}") - print( - f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - ) + #for P, linear_func in simplices_and_lin_funcs: + # print(f"P, linear_func = {P}, {linear_func}") + # for v in transBlock.simplex_point_indices: + # print(f" v={v}") + # print(f" pt={pw_linear_func._points[P[v]]}") + # print( + # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + # ) transBlock.set_substitute = Constraint( expr=substitute_var == sum( @@ -173,11 +165,10 @@ def x_constraint(b, i): ) ) - print("f") return substitute_var # Not a gray code, just a regular binary representation - # TODO this is probably not optimal, test the gray codes too + # TODO this may not be optimal, test the gray codes too def _get_binary_vector(self, num, length): if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") @@ -191,10 +182,4 @@ def _P_0(self, B, l, simplices): # Return {P \in \mathcal{P} | B(P)_l = 1} def _P_plus(self, B, l, simplices): - print(f"p plus: B={B}, l={l}, simplices={simplices}") - for p in simplices: - print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") - print( - f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" - ) return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From dbecedd3a1644fd1b940cf1cf9ab95e994e307e6 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 02:23:00 -0400 Subject: [PATCH 0483/1178] fix strange reverse indexing --- .../transform/disagreggated_logarithmic.py | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 00fb1546412..e86d5539367 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -15,17 +15,19 @@ "contrib.piecewise.disaggregated_logarithmic", doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """, ) class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() @@ -35,8 +37,8 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any). This is where we will put our new components. + # Get a new Block for our transformationin transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] @@ -61,32 +63,28 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) - # Enumeration of simplices, map from simplex number to simplex object + # Enumeration of simplices: map from simplex number to simplex object self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - # Inverse of previous enumeration - self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} - # List of tuples of simplices with their linear function - simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) # We don't seem to get a convenient opportunity later, so let's just widen # the bounds here. All we need to do is go through the corners of each simplex. - for P, linear_func in simplices_and_lin_funcs: + for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[P[v]]) + val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) if val < self.substitute_var_lb: self.substitute_var_lb = val if val > self.substitute_var_ub: self.substitute_var_ub = val # Now set those bounds - if self.substitute_var_lb < float('inf'): - transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float('inf'): - transBlock.substitute_var.setub(self.substitute_var_ub) + transBlock.substitute_var.setlb(self.substitute_var_lb) + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function @@ -115,22 +113,22 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc def simplex_choice_1(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_plus(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_plus(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= binaries[l] + <= transBlock.binaries[l] ) @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_0(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_0(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= 1 - binaries[l] + <= 1 - transBlock.binaries[l] ) # for i, (simplex, pwlf) in enumerate(choices): @@ -138,9 +136,9 @@ def simplex_choice_2(b, l): @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): return pw_expr.args[i] == sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * pw_linear_func._points[P[v]][i] - for P in simplices + transBlock.lambdas[P, v] + * pw_linear_func._points[self.idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices for v in transBlock.simplex_point_indices ) @@ -157,11 +155,11 @@ def x_constraint(b, i): expr=substitute_var == sum( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * linear_func(*pw_linear_func._points[P[v]]) + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) for v in transBlock.simplex_point_indices ) - for (P, linear_func) in simplices_and_lin_funcs + for (P, linear_func) in simplex_indices_and_lin_funcs ) ) @@ -177,9 +175,9 @@ def _get_binary_vector(self, num, length): return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + def _P_0(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 0] # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] + def _P_plus(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 1] From 8ff5d5ccd59679ce0deb8a254cfdb85e76e2dab3 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 9 Nov 2023 11:34:02 -0500 Subject: [PATCH 0484/1178] minor changes and add basic test for disaggregated log --- .../test_disaggregated_logarithmic_gdp.py | 36 +++++++++++++++++++ .../transform/disagreggated_logarithmic.py | 28 +++++---------- .../piecewise/transform/nested_inner_repn.py | 22 ++++++------ 3 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py new file mode 100644 index 00000000000..b3dc871882b --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var + +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import DisaggregatedLogarithmicInnerGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.disaggregated_logarithmic' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e86d5539367..a793ad77e38 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,13 +1,9 @@ -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet +from pyomo.core import Constraint, Binary, Var, RangeSet from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components from math import ceil, log2 @@ -37,7 +33,7 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block for our transformationin transformation_block.transformed_functions, + # Get a new Block for our transformation in transformation_block.transformed_functions, # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) @@ -78,7 +74,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc self.substitute_var_lb = val if val > self.substitute_var_ub: self.substitute_var_ub = val - # Now set those bounds transBlock.substitute_var.setlb(self.substitute_var_lb) transBlock.substitute_var.setub(self.substitute_var_ub) @@ -97,6 +92,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # for Nonseparable Piecewise-Linear Optimization + # Sum of all lambdas is one (6b) transBlock.convex_combo = Constraint( expr=sum( @@ -143,14 +141,6 @@ def x_constraint(b, i): ) # Make the substitute Var equal the PWLE (6a.2) - #for P, linear_func in simplices_and_lin_funcs: - # print(f"P, linear_func = {P}, {linear_func}") - # for v in transBlock.simplex_point_indices: - # print(f" v={v}") - # print(f" pt={pw_linear_func._points[P[v]]}") - # print( - # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - # ) transBlock.set_substitute = Constraint( expr=substitute_var == sum( @@ -165,13 +155,13 @@ def x_constraint(b, i): return substitute_var - # Not a gray code, just a regular binary representation - # TODO this may not be optimal, test the gray codes too + # Not a Gray code, just a regular binary representation + # TODO test the Gray codes too def _get_binary_vector(self, num, length): if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") - # Hack: use python's string formatting instead of bothering with modular - # arithmetic. May be slow. + # Use python's string formatting instead of bothering with modular + # arithmetic. Hopefully not slow. return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6c551818c84..a5c9b5015d3 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -28,7 +28,7 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -46,9 +46,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) - if self.DEBUG: - print(f"dimension is {self.dimension}") - # If there was only one choice, don't bother making a disjunction, just # use the linear function directly (but still use the substitute_var for # consistency). @@ -61,15 +58,12 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Add the disjunction transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) - # Widen bounds as determined when setting up the disjunction + # Set bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): transBlock.substitute_var.setlb(self.substitute_var_lb) if self.substitute_var_ub > -float('inf'): transBlock.substitute_var.setub(self.substitute_var_ub) - if self.DEBUG: - print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") - return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -77,8 +71,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # many calls deep. def _get_disjunction(self, choices, parent_block, pw_expr, root_block): size = len(choices) - if self.DEBUG: - print(f"calling _get_disjunction with size={size}") + # Our base cases will be 3 and 2, since it would be silly to construct # a Disjunction containing only one Disjunct. We can ensure that size # is never 1 unless it was only passsed a single choice from the start, @@ -88,7 +81,6 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): # This tree will be slightly heavier on the right side choices_l = choices[:half] choices_r = choices[half:] - # Is this valid Pyomo? @parent_block.Disjunct() def d_l(b): b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) @@ -124,32 +116,38 @@ def d_r(b): "_get_disjunction in nested_inner_repn.py.") def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): - # Define the lambdas sparsely like in the version I'm copying, + # Define the lambdas sparsely like in the normal inner repn, # only the first few will participate in constraints b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + # Get the extreme points to add up extreme_pts = [] for idx in simplex: extreme_pts.append(self.pw_linear_func._points[idx]) + # Constrain sum(lambda_i) = 1 b.convex_combo = Constraint( expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 ) linear_func_expr = linear_func(*pw_expr.args) + # Make the substitute Var equal the PWLE b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) if lb is not None and lb < self.substitute_var_lb: self.substitute_var_lb = lb if ub is not None and ub > self.substitute_var_ub: self.substitute_var_ub = ub + # Constrain x = \sum \lambda_i v_i @b.Constraint(range(self.dimension)) def linear_combo(d, i): return pw_expr.args[i] == sum( d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) ) + # Mark the lambdas as local in order to prevent disagreggating multiple # times in the hull transformation b.LocalVars = Suffix(direction=Suffix.LOCAL) From 6f4de26da622a35f446516d57e29e6acf5ded9e8 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 9 Nov 2023 11:38:21 -0500 Subject: [PATCH 0485/1178] apply black --- .../test_disaggregated_logarithmic_gdp.py | 18 ++- .../tests/test_nested_inner_repn_gdp.py | 18 ++- .../transform/disagreggated_logarithmic.py | 25 +++-- .../piecewise/transform/nested_inner_repn.py | 104 ++++++++++++------ 4 files changed, 99 insertions(+), 66 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py index b3dc871882b..d3b58f401f2 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py @@ -20,17 +20,15 @@ from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import Constraint, SolverFactory, Var -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import DisaggregatedLogarithmicInnerGDPTransformation +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( + DisaggregatedLogarithmicInnerGDPTransformation, +) -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): def test_solve_log_model(self): m = models.make_log_x_model() - TransformationFactory( - 'contrib.piecewise.disaggregated_logarithmic' - ).apply_to(m) - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) - SolverFactory('gurobi').solve(m) - ct.check_log_x_model_soln(self, m) \ No newline at end of file + TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) + TransformationFactory("gdp.bigm").apply_to(m) + SolverFactory("gurobi").solve(m) + ct.check_log_x_model_soln(self, m) diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 48357c828df..f41233435d4 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -20,17 +20,15 @@ from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import Constraint, SolverFactory, Var -from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation +from pyomo.contrib.piecewise.transform.nested_inner_repn import ( + NestedInnerRepresentationGDPTransformation, +) -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): def test_solve_log_model(self): m = models.make_log_x_model() - TransformationFactory( - 'contrib.piecewise.nested_inner_repn_gdp' - ).apply_to(m) - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) - SolverFactory('gurobi').solve(m) - ct.check_log_x_model_soln(self, m) \ No newline at end of file + TransformationFactory("contrib.piecewise.nested_inner_repn_gdp").apply_to(m) + TransformationFactory("gdp.bigm").apply_to(m) + SolverFactory("gurobi").solve(m) + ct.check_log_x_model_soln(self, m) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index a793ad77e38..8a9b493bdfe 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -20,9 +20,9 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we assume we have simplces in this code. This method is due to Vielma et al., 2010. """ @@ -32,8 +32,7 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - - # Get a new Block for our transformation in transformation_block.transformed_functions, + # Get a new Block for our transformation in transformation_block.transformed_functions, # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) @@ -60,12 +59,16 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.simplex_point_indices = RangeSet(0, dimension) # Enumeration of simplices: map from simplex number to simplex object - self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + self.idx_to_simplex = { + k: v for k, v in zip(transBlock.simplex_indices, simplices) + } # List of tuples of simplex indices with their linear function - simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) + simplex_indices_and_lin_funcs = list( + zip(transBlock.simplex_indices, pw_linear_func._linear_functions) + ) - # We don't seem to get a convenient opportunity later, so let's just widen + # We don't seem to get a convenient opportunity later, so let's just widen # the bounds here. All we need to do is go through the corners of each simplex. for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: @@ -90,9 +93,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc B[i] = self._get_binary_vector(i, log_dimension) # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it - transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + transBlock.lambdas = Var( + transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) + ) - # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models # for Nonseparable Piecewise-Linear Optimization # Sum of all lambdas is one (6b) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index a5c9b5015d3..af8728c0605 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -7,28 +7,29 @@ from pyomo.gdp import Disjunction from pyomo.common.errors import DeveloperError + @TransformationFactory.register( - 'contrib.piecewise.nested_inner_repn_gdp', - doc="TODO document", # TODO + "contrib.piecewise.nested_inner_repn_gdp", + doc="TODO document", # TODO ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): """ - Represent a piecewise linear function by using a nested GDP to determine - which polytope a point is in, then representing it as a convex combination - of extreme points, with multipliers "local" to that particular polytope, - i.e., not shared with neighbors. This method of formulating the piecewise - linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many binaries. - This method was, however, inspired by the disagreggated logarithmic + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic formulation of Vielma et al., 2010. """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_nested_inner_repn' - + _transformation_name = "pw_linear_nested_inner_repn" + # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -41,29 +42,35 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') - + self.substitute_var_lb = float("inf") + self.substitute_var_ub = -float("inf") + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) # If there was only one choice, don't bother making a disjunction, just - # use the linear function directly (but still use the substitute_var for + # use the linear function directly (but still use the substitute_var for # consistency). if len(choices) == 1: - (_, linear_func) = choices[0] # simplex isn't important in this case + (_, linear_func) = choices[0] # simplex isn't important in this case linear_func_expr = linear_func(*pw_expr.args) - transBlock.set_substitute = Constraint(expr=substitute_var == linear_func_expr) - (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) + transBlock.set_substitute = Constraint( + expr=substitute_var == linear_func_expr + ) + (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr( + linear_func_expr + ) else: # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) + transBlock.disj = self._get_disjunction( + choices, transBlock, pw_expr, transBlock + ) # Set bounds as determined when setting up the disjunction - if self.substitute_var_lb < float('inf'): + if self.substitute_var_lb < float("inf"): transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float('inf'): + if self.substitute_var_ub > -float("inf"): transBlock.substitute_var.setub(self.substitute_var_ub) - + return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -76,17 +83,24 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): # a Disjunction containing only one Disjunct. We can ensure that size # is never 1 unless it was only passsed a single choice from the start, # which we can handle before calling. - if size > 3: - half = size // 2 # (integer divide) + if size > 3: + half = size // 2 # (integer divide) # This tree will be slightly heavier on the right side choices_l = choices[:half] choices_r = choices[half:] + @parent_block.Disjunct() def d_l(b): - b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) + b.inner_disjunction_l = self._get_disjunction( + choices_l, b, pw_expr, root_block + ) + @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block) + b.inner_disjunction_r = self._get_disjunction( + choices_r, b, pw_expr, root_block + ) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 3: # Let's stay heavier on the right side for consistency. So the left @@ -95,27 +109,43 @@ def d_r(b): @parent_block.Disjunct() def d_l(b): simplex, linear_func = choices[0] - self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, root_block + ) + @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block) + b.inner_disjunction_r = self._get_disjunction( + choices[1:], b, pw_expr, root_block + ) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts @parent_block.Disjunct() def d_l(b): simplex, linear_func = choices[0] - self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, root_block + ) + @parent_block.Disjunct() def d_r(b): simplex, linear_func = choices[1] - self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, root_block + ) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) else: - raise DeveloperError("Unreachable: 1 or 0 choices were passed to " - "_get_disjunction in nested_inner_repn.py.") + raise DeveloperError( + "Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py." + ) - def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): + def _set_disjunct_block_constraints( + self, b, simplex, linear_func, pw_expr, root_block + ): # Define the lambdas sparsely like in the normal inner repn, # only the first few will participate in constraints b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) @@ -125,14 +155,16 @@ def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root for idx in simplex: extreme_pts.append(self.pw_linear_func._points[idx]) - # Constrain sum(lambda_i) = 1 + # Constrain sum(lambda_i) = 1 b.convex_combo = Constraint( expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 ) linear_func_expr = linear_func(*pw_expr.args) # Make the substitute Var equal the PWLE - b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + b.set_substitute = Constraint( + expr=root_block.substitute_var == linear_func_expr + ) # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) From c11fa709e5577fbff10b28d6b66aee0b71668304 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 13:16:51 -0500 Subject: [PATCH 0486/1178] rename: PiecewiseLinearToGDP->PiecewiseLinearTransformationBase, since it isn't GDP-specific --- .../transform/disagreggated_logarithmic.py | 6 +++--- .../transform/inner_representation_gdp.py | 6 +++--- .../piecewise/transform/nested_inner_repn.py | 17 +++++++++++++---- .../transform/outer_representation_gdp.py | 6 +++--- .../piecewise_to_gdp_transformation.py | 2 +- .../reduced_inner_representation_gdp.py | 6 +++--- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 8a9b493bdfe..e10c1ee8091 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,5 +1,5 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, Binary, Var, RangeSet from pyomo.core.base import TransformationFactory @@ -17,7 +17,7 @@ assume we have simplces in this code. This method is due to Vielma et al., 2010. """, ) -class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): +class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): """ Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; @@ -26,7 +26,7 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): assume we have simplces in this code. This method is due to Vielma et al., 2010. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_disaggregated_log" # Implement to use PiecewiseLinearToGDP. This function returns the Var diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index f0be2d98825..25b8664ccf2 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -11,7 +11,7 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -25,7 +25,7 @@ "simplices that are the domains of the linear " "functions.", ) -class InnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class InnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -49,7 +49,7 @@ class InnerRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_inner_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index af8728c0605..6900d1f3322 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -1,6 +1,6 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -10,9 +10,18 @@ @TransformationFactory.register( "contrib.piecewise.nested_inner_repn_gdp", - doc="TODO document", # TODO + doc=""" + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic + formulation of Vielma et al., 2010. + """ ) -class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Represent a piecewise linear function by using a nested GDP to determine which polytope a point is in, then representing it as a convex combination @@ -24,7 +33,7 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): formulation of Vielma et al., 2010. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_nested_inner_repn" # Implement to use PiecewiseLinearToGDP. This function returns the Var diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index 7c81619430a..bd50b9c708f 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -13,7 +13,7 @@ from pyomo.common.dependencies.scipy import spatial from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -27,7 +27,7 @@ "the simplices that are the domains of the " "linear functions.", ) -class OuterRepresentationGDPTransformation(PiecewiseLinearToGDP): +class OuterRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -49,7 +49,7 @@ class OuterRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_outer_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 2e056c47a15..d8e46ad7311 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -40,7 +40,7 @@ from pyomo.network import Port -class PiecewiseLinearToGDP(Transformation): +class PiecewiseLinearTransformationBase(Transformation): """ Base class for transformations of piecewise-linear models to GDPs """ diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index 5c7dfa895ab..86c33e40623 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -11,7 +11,7 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Var from pyomo.core.base import TransformationFactory @@ -25,7 +25,7 @@ "simplices that are the domains of the linear " "functions.", ) -class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -51,7 +51,7 @@ class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_reduced_inner_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): From f34bf30c73d3297cc88f33a21be0938379feafa3 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 13:21:37 -0500 Subject: [PATCH 0487/1178] apply black --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6900d1f3322..17b85604337 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -19,7 +19,7 @@ that this is NOT a logarithmic formulation - it has linearly many binaries. This method was, however, inspired by the disagreggated logarithmic formulation of Vielma et al., 2010. - """ + """, ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ From 7acb187a0737c885d1fb4e78cf5ca13c18908ea3 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:15:13 -0500 Subject: [PATCH 0488/1178] fix typos and add title for reference --- .../transform/disagreggated_logarithmic.py | 14 +++++++++----- .../piecewise/transform/nested_inner_repn.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e10c1ee8091..c6c492c70a3 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -11,10 +11,12 @@ "contrib.piecewise.disaggregated_logarithmic", doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplices in this code. This method is due to Vielma, Ahmed, + and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear + Optimization. """, ) class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): @@ -23,7 +25,9 @@ class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformati log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; GDP is not used. This method of logarithmically formulating the piecewise linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. + assume we have simplices in this code. This method is due to Vielma, Ahmed, + and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear + Optimization. """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 17b85604337..f6939a8a288 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -90,7 +90,7 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): # Our base cases will be 3 and 2, since it would be silly to construct # a Disjunction containing only one Disjunct. We can ensure that size - # is never 1 unless it was only passsed a single choice from the start, + # is never 1 unless it was only passed a single choice from the start, # which we can handle before calling. if size > 3: half = size // 2 # (integer divide) From 6548b6207217bd7ef48356275505242b8845fa15 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:18:31 -0500 Subject: [PATCH 0489/1178] fix incorrect imports and comments --- ...d_logarithmic_gdp.py => test_disaggregated_logarithmic.py} | 4 ++-- .../contrib/piecewise/transform/disagreggated_logarithmic.py | 2 +- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename pyomo/contrib/piecewise/tests/{test_disaggregated_logarithmic_gdp.py => test_disaggregated_logarithmic.py} (92%) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py similarity index 92% rename from pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py rename to pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index d3b58f401f2..8a225985d82 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -21,11 +21,11 @@ from pyomo.environ import Constraint, SolverFactory, Var from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( - DisaggregatedLogarithmicInnerGDPTransformation, + DisaggregatedLogarithmicInnerMIPTransformation ) -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): +class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): def test_solve_log_model(self): m = models.make_log_x_model() TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index c6c492c70a3..1104b1c265b 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -33,7 +33,7 @@ class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformati CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_disaggregated_log" - # Implement to use PiecewiseLinearToGDP. This function returns the Var + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): # Get a new Block for our transformation in transformation_block.transformed_functions, diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index f6939a8a288..7f5c54dd9a2 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -36,7 +36,7 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_nested_inner_repn" - # Implement to use PiecewiseLinearToGDP. This function returns the Var + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): # Get a new Block() in transformation_block.transformed_functions, which From ae953e894f4e3be1e9ae07dcf487978e9dbba25a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:22:00 -0500 Subject: [PATCH 0490/1178] register transformations in __init__.py so they don't need to be imported --- pyomo/contrib/piecewise/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 37873c83b3b..de18e559a93 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,3 +33,9 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.transform.nested_inner_repn import ( + NestedInnerRepresentationGDPTransformation, +) +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( + DisaggregatedLogarithmicInnerMIPTransformation, +) From 63af6266ab73d74547d837ee33b260bdadc71183 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:26:32 -0500 Subject: [PATCH 0491/1178] remove unused (for now) imports --- .../piecewise/tests/test_disaggregated_logarithmic.py | 11 +---------- .../piecewise/tests/test_nested_inner_repn_gdp.py | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 8a225985d82..8fd4bebfc37 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -13,16 +13,7 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.core.expr.compare import ( - assertExpressionsEqual, - assertExpressionsStructurallyEqual, -) -from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var - -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( - DisaggregatedLogarithmicInnerMIPTransformation -) +from pyomo.environ import SolverFactory class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index f41233435d4..fd8e7ab201c 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -13,16 +13,7 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.core.expr.compare import ( - assertExpressionsEqual, - assertExpressionsStructurallyEqual, -) -from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var - -from pyomo.contrib.piecewise.transform.nested_inner_repn import ( - NestedInnerRepresentationGDPTransformation, -) +from pyomo.environ import SolverFactory class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): From acfa476b9deb3ed3f2f141dd925fb2a54df3db3a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:34:26 -0500 Subject: [PATCH 0492/1178] do the reference properly --- .../transform/disagreggated_logarithmic.py | 24 ++++++++++--------- .../piecewise/transform/nested_inner_repn.py | 16 +++++++------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 1104b1c265b..4fa7a0eeeb1 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -12,22 +12,24 @@ doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplices in this code. This method is due to Vielma, Ahmed, - and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear - Optimization. + GDP is not used. """, ) class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplices in this code. This method is due to Vielma, Ahmed, - and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear - Optimization. + log_2(|P|) binary decision variables, following the "disaggregated logarithmic" + method from [1]. This is a direct-to-MIP transformation; GDP is not used. + This method of logarithmically formulating the piecewise linear function + imposes no restrictions on the family of polytopes, but we assume we have + simplices in this code. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 7f5c54dd9a2..67abd815c7f 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -14,11 +14,7 @@ Represent a piecewise linear function by using a nested GDP to determine which polytope a point is in, then representing it as a convex combination of extreme points, with multipliers "local" to that particular polytope, - i.e., not shared with neighbors. This method of formulating the piecewise - linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many binaries. - This method was, however, inspired by the disagreggated logarithmic - formulation of Vielma et al., 2010. + i.e., not shared with neighbors. This formulation has linearly many binaries. """, ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): @@ -29,8 +25,14 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa i.e., not shared with neighbors. This method of formulating the piecewise linear function imposes no restrictions on the family of polytopes. Note that this is NOT a logarithmic formulation - it has linearly many binaries. - This method was, however, inspired by the disagreggated logarithmic - formulation of Vielma et al., 2010. + However, it is inspired by the disaggregated logarithmic formulation of [1]. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() From 81960f146d22f54e00ab8db6a11e758545eca41f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Dec 2023 18:48:01 -0500 Subject: [PATCH 0493/1178] stop using `self` unnecessarily --- .../transform/disagreggated_logarithmic.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 4fa7a0eeeb1..4b388d9df1e 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -53,19 +53,19 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc pw_linear_func.map_transformation_var(pw_expr, substitute_var) # Bounds for the substitute_var that we will widen - self.substitute_var_lb = float("inf") - self.substitute_var_ub = -float("inf") + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") # Simplices are tuples of indices of points. Give them their own indices, too simplices = pw_linear_func._simplices num_simplices = len(simplices) transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really simplices and all have the same number of points, - # which is dimension + 1 + # Assumption: the simplices are really full-dimensional simplices and all have the + # same number of points, which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) # Enumeration of simplices: map from simplex number to simplex object - self.idx_to_simplex = { + idx_to_simplex = { k: v for k, v in zip(transBlock.simplex_indices, simplices) } @@ -78,13 +78,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # the bounds here. All we need to do is go through the corners of each simplex. for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) - if val < self.substitute_var_lb: - self.substitute_var_lb = val - if val > self.substitute_var_ub: - self.substitute_var_ub = val - transBlock.substitute_var.setlb(self.substitute_var_lb) - transBlock.substitute_var.setub(self.substitute_var_ub) + val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + if val < substitute_var_lb: + substitute_var_lb = val + if val > substitute_var_ub: + substitute_var_ub = val + transBlock.substitute_var.setlb(substitute_var_lb) + transBlock.substitute_var.setub(substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) @@ -146,7 +146,7 @@ def simplex_choice_2(b, l): def x_constraint(b, i): return pw_expr.args[i] == sum( transBlock.lambdas[P, v] - * pw_linear_func._points[self.idx_to_simplex[P][v]][i] + * pw_linear_func._points[idx_to_simplex[P][v]][i] for P in transBlock.simplex_indices for v in transBlock.simplex_point_indices ) @@ -157,7 +157,7 @@ def x_constraint(b, i): == sum( sum( transBlock.lambdas[P, v] - * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) + * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) for v in transBlock.simplex_point_indices ) for (P, linear_func) in simplex_indices_and_lin_funcs From c7975cdd48c1b058e15e81ec7e06bfa34c8bbe3a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Dec 2023 18:55:07 -0500 Subject: [PATCH 0494/1178] rename file to match refactored class name --- pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py | 2 +- pyomo/contrib/piecewise/transform/inner_representation_gdp.py | 2 +- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 2 +- pyomo/contrib/piecewise/transform/outer_representation_gdp.py | 2 +- ...ransformation.py => piecewise_linear_transformation_base.py} | 0 .../piecewise/transform/reduced_inner_representation_gdp.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename pyomo/contrib/piecewise/transform/{piecewise_to_gdp_transformation.py => piecewise_linear_transformation_base.py} (100%) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 4b388d9df1e..4e399d714f7 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,4 +1,4 @@ -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, Binary, Var, RangeSet diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index 25b8664ccf2..e4818c1cbb9 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 67abd815c7f..e7e76dc3778 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -1,5 +1,5 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index bd50b9c708f..6c26772fe6a 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -12,7 +12,7 @@ import pyomo.common.dependencies.numpy as np from pyomo.common.dependencies.scipy import spatial from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py similarity index 100% rename from pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py rename to pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index 86c33e40623..a19507a93fd 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Var From 526305c6c3c87c918bed3f1be4e1f43bc325dea9 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Dec 2023 22:06:24 -0500 Subject: [PATCH 0495/1178] minor refactors --- .../piecewise/transform/nested_inner_repn.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index e7e76dc3778..6ee0e6c9e80 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -14,7 +14,8 @@ Represent a piecewise linear function by using a nested GDP to determine which polytope a point is in, then representing it as a convex combination of extreme points, with multipliers "local" to that particular polytope, - i.e., not shared with neighbors. This formulation has linearly many binaries. + i.e., not shared with neighbors. This formulation has linearly many Boolean + variables, though up to variable substitution, it has logarithmically many. """, ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): @@ -24,8 +25,10 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa of extreme points, with multipliers "local" to that particular polytope, i.e., not shared with neighbors. This method of formulating the piecewise linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many binaries. - However, it is inspired by the disaggregated logarithmic formulation of [1]. + that this is NOT a logarithmic formulation - it has linearly many Boolean + variables. However, it is inspired by the disaggregated logarithmic + formulation of [1]. Up to variable substitution, the amount of Boolean + variables is logarithmic, as in [1]. References ---------- @@ -47,14 +50,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc len(transformation_block.transformed_functions) ] - # these copy-pasted lines (from inner_representation_gdp) seem useful - # adding some of this stuff to self so I don't have to pass it around - self.pw_linear_func = pw_linear_func - self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float("inf") - self.substitute_var_ub = -float("inf") + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) @@ -67,27 +66,27 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr( + (substitute_var_lb, substitute_var_ub) = compute_bounds_on_expr( linear_func_expr ) else: # Add the disjunction transBlock.disj = self._get_disjunction( - choices, transBlock, pw_expr, transBlock + choices, transBlock, pw_expr, pw_linear_func, transBlock ) # Set bounds as determined when setting up the disjunction - if self.substitute_var_lb < float("inf"): - transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float("inf"): - transBlock.substitute_var.setub(self.substitute_var_ub) + if substitute_var_lb < float("inf"): + transBlock.substitute_var.setlb(substitute_var_lb) + if substitute_var_ub > -float("inf"): + transBlock.substitute_var.setub(substitute_var_ub) return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, root_block): + def _get_disjunction(self, choices, parent_block, pw_expr, pw_linear_func, root_block): size = len(choices) # Our base cases will be 3 and 2, since it would be silly to construct @@ -103,13 +102,13 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): @parent_block.Disjunct() def d_l(b): b.inner_disjunction_l = self._get_disjunction( - choices_l, b, pw_expr, root_block + choices_l, b, pw_expr, pw_linear_func, root_block ) @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction( - choices_r, b, pw_expr, root_block + choices_r, b, pw_expr, pw_linear_func, root_block ) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) @@ -121,13 +120,13 @@ def d_r(b): def d_l(b): simplex, linear_func = choices[0] self._set_disjunct_block_constraints( - b, simplex, linear_func, pw_expr, root_block + b, simplex, linear_func, pw_expr, pw_linear_func, root_block ) @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction( - choices[1:], b, pw_expr, root_block + choices[1:], b, pw_expr, pw_linear_func, root_block ) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) @@ -137,14 +136,14 @@ def d_r(b): def d_l(b): simplex, linear_func = choices[0] self._set_disjunct_block_constraints( - b, simplex, linear_func, pw_expr, root_block + b, simplex, linear_func, pw_expr, pw_linear_func, root_block ) @parent_block.Disjunct() def d_r(b): simplex, linear_func = choices[1] self._set_disjunct_block_constraints( - b, simplex, linear_func, pw_expr, root_block + b, simplex, linear_func, pw_expr, pw_linear_func, root_block ) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) @@ -155,7 +154,7 @@ def d_r(b): ) def _set_disjunct_block_constraints( - self, b, simplex, linear_func, pw_expr, root_block + self, b, simplex, linear_func, pw_expr, pw_linear_func, root_block ): # Define the lambdas sparsely like in the normal inner repn, # only the first few will participate in constraints @@ -164,7 +163,7 @@ def _set_disjunct_block_constraints( # Get the extreme points to add up extreme_pts = [] for idx in simplex: - extreme_pts.append(self.pw_linear_func._points[idx]) + extreme_pts.append(pw_linear_func._points[idx]) # Constrain sum(lambda_i) = 1 b.convex_combo = Constraint( @@ -179,13 +178,13 @@ def _set_disjunct_block_constraints( # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) - if lb is not None and lb < self.substitute_var_lb: - self.substitute_var_lb = lb - if ub is not None and ub > self.substitute_var_ub: - self.substitute_var_ub = ub + if lb is not None and lb < substitute_var_lb: + substitute_var_lb = lb + if ub is not None and ub > substitute_var_ub: + substitute_var_ub = ub # Constrain x = \sum \lambda_i v_i - @b.Constraint(range(self.dimension)) + @b.Constraint(range(pw_expr.nargs())) # dimension def linear_combo(d, i): return pw_expr.args[i] == sum( d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) From d6b9f8829dfe1d436a754955d29d1030269d791a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 21 Dec 2023 19:26:43 -0500 Subject: [PATCH 0496/1178] Fix needless quadratic loops I think this also could've been achieved by reordering the iterators, but using indexed Sets should be more clear --- .../transform/disagreggated_logarithmic.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 4e399d714f7..e22bba0215f 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -98,6 +98,18 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # map index(P) -> corresponding vector in {0, 1}^n B[i] = self._get_binary_vector(i, log_dimension) + # Build up P_0 and P_plus ahead of time. + + # {P \in \mathcal{P} | B(P)_l = 0} + @transBlock.Set(transBlock.simplex_indices) + def P_0(l): + return [p for p in transBlock.simplex_indices if B[p][l] == 0] + + # {P \in \mathcal{P} | B(P)_l = 1} + @transBlock.Set(transBlock.simplex_indices) + def P_0(l): + return [p for p in transBlock.simplex_indices if B[p][l] == 1] + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var( transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) @@ -116,14 +128,14 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc == 1 ) - # The branching rules, establishing using the binaries that only one simplex's lambdas - # may be nonzero + # The branching rules, establishing using the binaries that only one simplex's lambda + # coefficients may be nonzero @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): return ( sum( transBlock.lambdas[P, v] - for P in self._P_plus(B, l, transBlock.simplex_indices) + for P in transBlock.P_plus[l] for v in transBlock.simplex_point_indices ) <= transBlock.binaries[l] @@ -134,7 +146,7 @@ def simplex_choice_2(b, l): return ( sum( transBlock.lambdas[P, v] - for P in self._P_0(B, l, transBlock.simplex_indices) + for P in transBlock.P_0[l] for v in transBlock.simplex_point_indices ) <= 1 - transBlock.binaries[l] @@ -174,11 +186,3 @@ def _get_binary_vector(self, num, length): # Use python's string formatting instead of bothering with modular # arithmetic. Hopefully not slow. return tuple(int(x) for x in format(num, f"0{length}b")) - - # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 0] - - # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 1] From 1248806195707f1a27e49ecc3dd9b1b1e2e9c794 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 21 Dec 2023 20:41:43 -0500 Subject: [PATCH 0497/1178] fix scoping error --- .../piecewise/transform/nested_inner_repn.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6ee0e6c9e80..143bd827c58 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -52,8 +52,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - substitute_var_lb = float("inf") - substitute_var_ub = -float("inf") + transBlock.substitute_var_lb = float("inf") + transBlock.substitute_var_ub = -float("inf") choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) @@ -66,7 +66,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - (substitute_var_lb, substitute_var_ub) = compute_bounds_on_expr( + (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = compute_bounds_on_expr( linear_func_expr ) else: @@ -76,10 +76,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc ) # Set bounds as determined when setting up the disjunction - if substitute_var_lb < float("inf"): - transBlock.substitute_var.setlb(substitute_var_lb) - if substitute_var_ub > -float("inf"): - transBlock.substitute_var.setub(substitute_var_ub) + if transBlock.substitute_var_lb < float("inf"): + transBlock.substitute_var.setlb(transBlock.substitute_var_lb) + if transBlock.substitute_var_ub > -float("inf"): + transBlock.substitute_var.setub(transBlock.substitute_var_ub) return substitute_var @@ -178,10 +178,10 @@ def _set_disjunct_block_constraints( # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) - if lb is not None and lb < substitute_var_lb: - substitute_var_lb = lb - if ub is not None and ub > substitute_var_ub: - substitute_var_ub = ub + if lb is not None and lb < root_block.substitute_var_lb: + root_block.substitute_var_lb = lb + if ub is not None and ub > root_block.substitute_var_ub: + root_block.substitute_var_ub = ub # Constrain x = \sum \lambda_i v_i @b.Constraint(range(pw_expr.nargs())) # dimension From 18821c7a4db6934705f6d7dc4ba463880e583137 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Dec 2023 13:55:48 -0500 Subject: [PATCH 0498/1178] proper testing for nested_inner_repn_gdp --- .../tests/test_nested_inner_repn_gdp.py | 169 +++++++++++++++++- 1 file changed, 167 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index fd8e7ab201c..4deeb9abe78 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -13,10 +13,175 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.environ import SolverFactory - +from pyomo.environ import SolverFactory, Var, Constraint +from pyomo.gdp import Disjunction, Disjunct +from pyomo.core.expr.compare import assertExpressionsEqual +# Test the nested inner repn gdp model using the common_tests code class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + # Check one disjunct for proper contents. Disjunct structure should be + # identical to the version for the inner representation gdp + def check_log_disjunct(self, d, pts, f, substitute_var, x): + self.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + self.assertEqual(len(d.component_map(Var)), 2) + self.assertIsInstance(d.lambdas, Var) + self.assertEqual(len(d.lambdas), 2) + for lamb in d.lambdas.values(): + self.assertEqual(lamb.lb, 0) + self.assertEqual(lamb.ub, 1) + self.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 + ) + self.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + self, d.set_substitute.expr, substitute_var == f(x), places=7 + ) + self.assertIsInstance(d.linear_combo, Constraint) + self.assertEqual(len(d.linear_combo), 1) + assertExpressionsEqual( + self, + d.linear_combo[0].expr, + x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], + ) + + # Check one disjunct from the paraboloid block for proper contents. This should + # be identical to the inner_representation_gdp one + def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): + self.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + self.assertEqual(len(d.component_map(Var)), 2) + self.assertIsInstance(d.lambdas, Var) + self.assertEqual(len(d.lambdas), 3) + for lamb in d.lambdas.values(): + self.assertEqual(lamb.lb, 0) + self.assertEqual(lamb.ub, 1) + self.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 + ) + self.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 + ) + self.assertIsInstance(d.linear_combo, Constraint) + self.assertEqual(len(d.linear_combo), 2) + assertExpressionsEqual( + self, + d.linear_combo[0].expr, + x1 + == pts[0][0] * d.lambdas[0] + + pts[1][0] * d.lambdas[1] + + pts[2][0] * d.lambdas[2], + ) + assertExpressionsEqual( + self, + d.linear_combo[1].expr, + x2 + == pts[0][1] * d.lambdas[0] + + pts[1][1] * d.lambdas[1] + + pts[2][1] * d.lambdas[2], + ) + + + # Check the structure of the log PWLF Block + def check_pw_log(self, m): + z = m.pw_log.get_transformation_var(m.log_expr) + self.assertIsInstance(z, Var) + # Now we can use those Vars to check on what the transformation created + log_block = z.parent_block() + + # Not using ct.check_trans_block_structure() because these are slightly + # different + # Two top-level disjuncts + self.assertEqual(len(log_block.component_map(Disjunct)), 2) + # One disjunction + self.assertEqual(len(log_block.component_map(Disjunction)), 1) + # The 'z' var (that we will substitute in for the function being + # approximated) is here: + self.assertEqual(len(log_block.component_map(Var)), 1) + self.assertIsInstance(log_block.substitute_var, Var) + + # Check the tree structure, which should be heavier on the right + # Parent disjunction + self.assertIsInstance(log_block.disj, Disjunction) + self.assertEqual(len(log_block.disj.disjuncts), 2) + + # Left disjunct with constraints + self.assertIsInstance(log_block.d_l, Disjunct) + self.check_log_disjunct(log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x) + + # Right disjunct with disjunction + self.assertIsInstance(log_block.d_r, Disjunct) + self.assertIsInstance(log_block.d_r.inner_disjunction_r, Disjunction) + self.assertEqual(len(log_block.d_r.inner_disjunction_r.disjuncts), 2) + + # Left and right child disjuncts with constraints + self.assertIsInstance(log_block.d_r.d_l, Disjunct) + self.check_log_disjunct(log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x) + self.assertIsInstance(log_block.d_r.d_r, Disjunct) + self.check_log_disjunct(log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x) + + # Check that this also became the objective + self.assertIs(m.obj.expr.expr, log_block.substitute_var) + + # Check the structure of the paraboloid PWLF block + def check_pw_paraboloid(self, m): + z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr) + self.assertIsInstance(z, Var) + paraboloid_block = z.parent_block() + + # Two top-level disjuncts + self.assertEqual(len(paraboloid_block.component_map(Disjunct)), 2) + # One disjunction + self.assertEqual(len(paraboloid_block.component_map(Disjunction)), 1) + # The 'z' var (that we will substitute in for the function being + # approximated) is here: + self.assertEqual(len(paraboloid_block.component_map(Var)), 1) + self.assertIsInstance(paraboloid_block.substitute_var, Var) + + # This one should have an even tree with four leaf disjuncts + disjuncts_dict = { + paraboloid_block.d_l.d_l: ([(0, 1), (0, 4), (3, 4)], m.g1), + paraboloid_block.d_l.d_r: ([(0, 1), (3, 4), (3, 1)], m.g1), + paraboloid_block.d_r.d_l: ([(3, 4), (3, 7), (0, 7)], m.g2), + paraboloid_block.d_r.d_r: ([(0, 7), (0, 4), (3, 4)], m.g2), + } + for d, (pts, f) in disjuncts_dict.items(): + self.check_paraboloid_disjunct( + d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + ) + + # And check the substitute Var is in the objective now. + self.assertIs(m.indexed_c[0].body.args[0].expr, paraboloid_block.substitute_var) + + # Test methods using the common_tests.py code. Copied in from test_inner_repn_gdp.py. + def test_transformation_do_not_descend(self): + ct.check_transformation_do_not_descend(self, 'contrib.piecewise.nested_inner_repn_gdp') + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions(self, 'contrib.piecewise.nested_inner_repn_gdp') + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + # Check the solution of the log(x) model + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_log_model(self): m = models.make_log_x_model() TransformationFactory("contrib.piecewise.nested_inner_repn_gdp").apply_to(m) From d1a92e0789ec6c9bf217ddbae41b428b782aeb6f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Dec 2023 13:57:42 -0500 Subject: [PATCH 0499/1178] apply black --- .../tests/test_nested_inner_repn_gdp.py | 27 ++++++++++++------- .../transform/disagreggated_logarithmic.py | 6 ++--- .../piecewise/transform/nested_inner_repn.py | 17 +++++++----- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 4deeb9abe78..8e8e4530d2c 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -17,10 +17,10 @@ from pyomo.gdp import Disjunction, Disjunct from pyomo.core.expr.compare import assertExpressionsEqual + # Test the nested inner repn gdp model using the common_tests code class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): - - # Check one disjunct for proper contents. Disjunct structure should be + # Check one disjunct for proper contents. Disjunct structure should be # identical to the version for the inner representation gdp def check_log_disjunct(self, d, pts, f, substitute_var, x): self.assertEqual(len(d.component_map(Constraint)), 3) @@ -85,7 +85,6 @@ def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): + pts[2][1] * d.lambdas[2], ) - # Check the structure of the log PWLF Block def check_pw_log(self, m): z = m.pw_log.get_transformation_var(m.log_expr) @@ -111,7 +110,9 @@ def check_pw_log(self, m): # Left disjunct with constraints self.assertIsInstance(log_block.d_l, Disjunct) - self.check_log_disjunct(log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x) + self.check_log_disjunct( + log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x + ) # Right disjunct with disjunction self.assertIsInstance(log_block.d_r, Disjunct) @@ -120,9 +121,13 @@ def check_pw_log(self, m): # Left and right child disjuncts with constraints self.assertIsInstance(log_block.d_r.d_l, Disjunct) - self.check_log_disjunct(log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x) + self.check_log_disjunct( + log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x + ) self.assertIsInstance(log_block.d_r.d_r, Disjunct) - self.check_log_disjunct(log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x) + self.check_log_disjunct( + log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x + ) # Check that this also became the objective self.assertIs(m.obj.expr.expr, log_block.substitute_var) @@ -156,10 +161,12 @@ def check_pw_paraboloid(self, m): # And check the substitute Var is in the objective now. self.assertIs(m.indexed_c[0].body.args[0].expr, paraboloid_block.substitute_var) - + # Test methods using the common_tests.py code. Copied in from test_inner_repn_gdp.py. def test_transformation_do_not_descend(self): - ct.check_transformation_do_not_descend(self, 'contrib.piecewise.nested_inner_repn_gdp') + ct.check_transformation_do_not_descend( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) def test_transformation_PiecewiseLinearFunction_targets(self): ct.check_transformation_PiecewiseLinearFunction_targets( @@ -167,7 +174,9 @@ def test_transformation_PiecewiseLinearFunction_targets(self): ) def test_descend_into_expressions(self): - ct.check_descend_into_expressions(self, 'contrib.piecewise.nested_inner_repn_gdp') + ct.check_descend_into_expressions( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) def test_descend_into_expressions_constraint_target(self): ct.check_descend_into_expressions_constraint_target( diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e22bba0215f..b54be3d0b75 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -60,14 +60,12 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc simplices = pw_linear_func._simplices num_simplices = len(simplices) transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really full-dimensional simplices and all have the + # Assumption: the simplices are really full-dimensional simplices and all have the # same number of points, which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) # Enumeration of simplices: map from simplex number to simplex object - idx_to_simplex = { - k: v for k, v in zip(transBlock.simplex_indices, simplices) - } + idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} # List of tuples of simplex indices with their linear function simplex_indices_and_lin_funcs = list( diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 143bd827c58..97bfd9316f4 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -25,8 +25,8 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa of extreme points, with multipliers "local" to that particular polytope, i.e., not shared with neighbors. This method of formulating the piecewise linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many Boolean - variables. However, it is inspired by the disaggregated logarithmic + that this is NOT a logarithmic formulation - it has linearly many Boolean + variables. However, it is inspired by the disaggregated logarithmic formulation of [1]. Up to variable substitution, the amount of Boolean variables is logarithmic, as in [1]. @@ -66,9 +66,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = compute_bounds_on_expr( - linear_func_expr - ) + ( + transBlock.substitute_var_lb, + transBlock.substitute_var_ub, + ) = compute_bounds_on_expr(linear_func_expr) else: # Add the disjunction transBlock.disj = self._get_disjunction( @@ -86,7 +87,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, pw_linear_func, root_block): + def _get_disjunction( + self, choices, parent_block, pw_expr, pw_linear_func, root_block + ): size = len(choices) # Our base cases will be 3 and 2, since it would be silly to construct @@ -184,7 +187,7 @@ def _set_disjunct_block_constraints( root_block.substitute_var_ub = ub # Constrain x = \sum \lambda_i v_i - @b.Constraint(range(pw_expr.nargs())) # dimension + @b.Constraint(range(pw_expr.nargs())) # dimension def linear_combo(d, i): return pw_expr.args[i] == sum( d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) From 14aa6fcb6a065db500bcd47360cecb7e0659649a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Dec 2023 15:02:10 -0500 Subject: [PATCH 0500/1178] fix initialization bug --- .../piecewise/transform/disagreggated_logarithmic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index b54be3d0b75..b04f66584d2 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,7 +1,7 @@ from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) -from pyomo.core import Constraint, Binary, Var, RangeSet +from pyomo.core import Constraint, Binary, Var, RangeSet, Set from pyomo.core.base import TransformationFactory from pyomo.common.errors import DeveloperError from math import ceil, log2 @@ -99,14 +99,14 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Build up P_0 and P_plus ahead of time. # {P \in \mathcal{P} | B(P)_l = 0} - @transBlock.Set(transBlock.simplex_indices) - def P_0(l): + def P_0_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 0] + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) # {P \in \mathcal{P} | B(P)_l = 1} - @transBlock.Set(transBlock.simplex_indices) - def P_0(l): + def P_plus_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 1] + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var( From 98ed11940aff5f7b0b82474712544d15d2f8a72f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 15 Feb 2024 15:00:42 -0500 Subject: [PATCH 0501/1178] Fix up tests for disaggreggated logarithmic --- .../tests/test_disaggregated_logarithmic.py | 267 +++++++++++++++++- .../transform/disagreggated_logarithmic.py | 4 + 2 files changed, 269 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 8fd4bebfc37..cb77ec844fe 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -13,13 +13,276 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.environ import SolverFactory +from pyomo.environ import SolverFactory, Var, Constraint +from pyomo.core.expr.compare import assertExpressionsEqual class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): + def check_pw_log(self, m): + z = m.pw_log.get_transformation_var(m.log_expr) + self.assertIsInstance(z, Var) + # Now we can use those Vars to check on what the transformation created + log_block = z.parent_block() + + # We should have three Vars, two of which are indexed, and five + # Constraints, three of which are indexed + + self.assertEqual(len(log_block.component_map(Var)), 3) + self.assertEqual(len(log_block.component_map(Constraint)), 5) + + # Constants + simplex_count = 3 + log_simplex_count = 2 + simplex_point_count = 2 + + # Substitute var + self.assertIsInstance(log_block.substitute_var, Var) + self.assertIs(m.obj.expr.expr, log_block.substitute_var) + # Binaries + self.assertIsInstance(log_block.binaries, Var) + self.assertEqual(len(log_block.binaries), log_simplex_count) + # Lambdas + self.assertIsInstance(log_block.lambdas, Var) + self.assertEqual(len(log_block.lambdas), simplex_count * simplex_point_count) + for l in log_block.lambdas.values(): + self.assertEqual(l.lb, 0) + self.assertEqual(l.ub, 1) + + # Convex combo constraint + self.assertIsInstance(log_block.convex_combo, Constraint) + assertExpressionsEqual( + self, + log_block.convex_combo.expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[1, 0] + + log_block.lambdas[1, 1] + + log_block.lambdas[2, 0] + + log_block.lambdas[2, 1] + == 1, + ) + + # Set substitute constraint + self.assertIsInstance(log_block.set_substitute, Constraint) + assertExpressionsEqual( + self, + log_block.set_substitute.expr, + log_block.substitute_var + == log_block.lambdas[0, 0] * m.f1(1) + + log_block.lambdas[0, 1] * m.f1(3) + + log_block.lambdas[1, 0] * m.f2(3) + + log_block.lambdas[1, 1] * m.f2(6) + + log_block.lambdas[2, 0] * m.f3(6) + + log_block.lambdas[2, 1] * m.f3(10), + places=7, + ) + + # x constraint + self.assertIsInstance(log_block.x_constraint, Constraint) + # one-dimensional case, so there is only one x variable here + self.assertEqual(len(log_block.x_constraint), 1) + assertExpressionsEqual( + self, + log_block.x_constraint[0].expr, + m.x + == 1 * log_block.lambdas[0, 0] + + 3 * log_block.lambdas[0, 1] + + 3 * log_block.lambdas[1, 0] + + 6 * log_block.lambdas[1, 1] + + 6 * log_block.lambdas[2, 0] + + 10 * log_block.lambdas[2, 1], + ) + + # simplex choice 1 constraint enables lambdas when binaries are on + self.assertEqual(len(log_block.simplex_choice_1), log_simplex_count) + assertExpressionsEqual( + self, + log_block.simplex_choice_1[0].expr, + log_block.lambdas[2, 0] + log_block.lambdas[2, 1] <= log_block.binaries[0], + ) + assertExpressionsEqual( + self, + log_block.simplex_choice_1[1].expr, + log_block.lambdas[1, 0] + log_block.lambdas[1, 1] <= log_block.binaries[1], + ) + # simplex choice 2 constraint enables lambdas when binaries are off + self.assertEqual(len(log_block.simplex_choice_2), log_simplex_count) + assertExpressionsEqual( + self, + log_block.simplex_choice_2[0].expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[1, 0] + + log_block.lambdas[1, 1] + <= 1 - log_block.binaries[0], + ) + assertExpressionsEqual( + self, + log_block.simplex_choice_2[1].expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[2, 0] + + log_block.lambdas[2, 1] + <= 1 - log_block.binaries[1], + ) + + def check_pw_paraboloid(self, m): + # This is a little larger, but at least test that the right numbers of + # everything are created + z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr) + self.assertIsInstance(z, Var) + paraboloid_block = z.parent_block() + + self.assertEqual(len(paraboloid_block.component_map(Var)), 3) + self.assertEqual(len(paraboloid_block.component_map(Constraint)), 5) + + # Constants + simplex_count = 4 + log_simplex_count = 2 + simplex_point_count = 3 + + # Substitute var + self.assertIsInstance(paraboloid_block.substitute_var, Var) + # assertExpressionsEqual( + # self, + # m.indexed_c[0].expr, + # m.x >= paraboloid_block.substitute_var + # ) + # Binaries + self.assertIsInstance(paraboloid_block.binaries, Var) + self.assertEqual(len(paraboloid_block.binaries), log_simplex_count) + # Lambdas + self.assertIsInstance(paraboloid_block.lambdas, Var) + # print(f"the lambdas are: {paraboloid_block.lambdas.pprint()}") + self.assertEqual( + len(paraboloid_block.lambdas), simplex_count * simplex_point_count + ) + for l in paraboloid_block.lambdas.values(): + self.assertEqual(l.lb, 0) + self.assertEqual(l.ub, 1) + + # Convex combo constraint + self.assertIsInstance(paraboloid_block.convex_combo, Constraint) + assertExpressionsEqual( + self, + paraboloid_block.convex_combo.expr, + paraboloid_block.lambdas[0, 0] + + paraboloid_block.lambdas[0, 1] + + paraboloid_block.lambdas[0, 2] + + paraboloid_block.lambdas[1, 0] + + paraboloid_block.lambdas[1, 1] + + paraboloid_block.lambdas[1, 2] + + paraboloid_block.lambdas[2, 0] + + paraboloid_block.lambdas[2, 1] + + paraboloid_block.lambdas[2, 2] + + paraboloid_block.lambdas[3, 0] + + paraboloid_block.lambdas[3, 1] + + paraboloid_block.lambdas[3, 2] + == 1, + ) + + # Set substitute constraint + self.assertIsInstance(paraboloid_block.set_substitute, Constraint) + assertExpressionsEqual( + self, + paraboloid_block.set_substitute.expr, + paraboloid_block.substitute_var + == paraboloid_block.lambdas[0, 0] * m.g1(0, 1) + + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) + + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 0] * m.g1(0, 1) + + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + + paraboloid_block.lambdas[2, 0] * m.g2(3, 4) + + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) + + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + + paraboloid_block.lambdas[3, 0] * m.g2(0, 7) + + paraboloid_block.lambdas[3, 1] * m.g2(0, 4) + + paraboloid_block.lambdas[3, 2] * m.g2(3, 4), + places=7, + ) + + # x constraint + self.assertIsInstance(paraboloid_block.x_constraint, Constraint) + # Here we have two x variables + self.assertEqual(len(paraboloid_block.x_constraint), 2) + assertExpressionsEqual( + self, + paraboloid_block.x_constraint[0].expr, + m.x1 + == 0 * paraboloid_block.lambdas[0, 0] + + 0 * paraboloid_block.lambdas[0, 1] + + 3 * paraboloid_block.lambdas[0, 2] + + 0 * paraboloid_block.lambdas[1, 0] + + 3 * paraboloid_block.lambdas[1, 1] + + 3 * paraboloid_block.lambdas[1, 2] + + 3 * paraboloid_block.lambdas[2, 0] + + 3 * paraboloid_block.lambdas[2, 1] + + 0 * paraboloid_block.lambdas[2, 2] + + 0 * paraboloid_block.lambdas[3, 0] + + 0 * paraboloid_block.lambdas[3, 1] + + 3 * paraboloid_block.lambdas[3, 2], + ) + assertExpressionsEqual( + self, + paraboloid_block.x_constraint[1].expr, + m.x2 + == 1 * paraboloid_block.lambdas[0, 0] + + 4 * paraboloid_block.lambdas[0, 1] + + 4 * paraboloid_block.lambdas[0, 2] + + 1 * paraboloid_block.lambdas[1, 0] + + 4 * paraboloid_block.lambdas[1, 1] + + 1 * paraboloid_block.lambdas[1, 2] + + 4 * paraboloid_block.lambdas[2, 0] + + 7 * paraboloid_block.lambdas[2, 1] + + 7 * paraboloid_block.lambdas[2, 2] + + 7 * paraboloid_block.lambdas[3, 0] + + 4 * paraboloid_block.lambdas[3, 1] + + 4 * paraboloid_block.lambdas[3, 2], + ) + + # The choices will get long, so let's just assert we have enough + self.assertEqual(len(paraboloid_block.simplex_choice_1), log_simplex_count) + self.assertEqual(len(paraboloid_block.simplex_choice_2), log_simplex_count) + + # Test methods using the common_tests.py code. + def test_transformation_do_not_descend(self): + ct.check_transformation_do_not_descend( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + # Check solution of the log(x) model + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_log_model(self): m = models.make_log_x_model() TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) - TransformationFactory("gdp.bigm").apply_to(m) SolverFactory("gurobi").solve(m) ct.check_log_x_model_soln(self, m) + + @unittest.skipIf(True, reason="because") + def test_test(self): + m = models.make_log_x_model() + TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) + m.pprint() + assert False diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index b04f66584d2..009282aa310 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -101,11 +101,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # {P \in \mathcal{P} | B(P)_l = 0} def P_0_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 0] + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) # {P \in \mathcal{P} | B(P)_l = 1} def P_plus_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 1] + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it @@ -128,6 +130,7 @@ def P_plus_init(m, l): # The branching rules, establishing using the binaries that only one simplex's lambda # coefficients may be nonzero + # Enabling lambdas when binaries are on @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): return ( @@ -139,6 +142,7 @@ def simplex_choice_1(b, l): <= transBlock.binaries[l] ) + # Disabling lambdas when binaries are on @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( From 1312af5dd0fb1c97b69dd9345999ddc134f2391f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 15 Feb 2024 15:30:51 -0500 Subject: [PATCH 0502/1178] fix name typo --- pyomo/contrib/piecewise/__init__.py | 2 +- .../piecewise/tests/test_disaggregated_logarithmic.py | 7 ------- ...ggated_logarithmic.py => disaggreggated_logarithmic.py} | 0 3 files changed, 1 insertion(+), 8 deletions(-) rename pyomo/contrib/piecewise/transform/{disagreggated_logarithmic.py => disaggreggated_logarithmic.py} (100%) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index de18e559a93..8a66af89bad 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -36,6 +36,6 @@ from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( +from pyomo.contrib.piecewise.transform.disaggreggated_logarithmic import ( DisaggregatedLogarithmicInnerMIPTransformation, ) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index cb77ec844fe..49a30d0996c 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -279,10 +279,3 @@ def test_solve_log_model(self): TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) SolverFactory("gurobi").solve(m) ct.check_log_x_model_soln(self, m) - - @unittest.skipIf(True, reason="because") - def test_test(self): - m = models.make_log_x_model() - TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) - m.pprint() - assert False diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py similarity index 100% rename from pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py rename to pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py From b9a3e4426a101ce6483b549fe26e9f8dc32d00ac Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 15 Feb 2024 15:52:12 -0500 Subject: [PATCH 0503/1178] satisfy black? --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 97bfd9316f4..b273c9776a2 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -66,10 +66,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - ( - transBlock.substitute_var_lb, - transBlock.substitute_var_ub, - ) = compute_bounds_on_expr(linear_func_expr) + (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = ( + compute_bounds_on_expr(linear_func_expr) + ) else: # Add the disjunction transBlock.disj = self._get_disjunction( From 133d0ff3e7871d9cae5209c7577c947ba94b773e Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 20 Feb 2024 18:30:20 -0500 Subject: [PATCH 0504/1178] update and add copyright notices --- .../piecewise/tests/test_disaggregated_logarithmic.py | 2 +- .../piecewise/tests/test_nested_inner_repn_gdp.py | 2 +- .../piecewise/transform/disaggreggated_logarithmic.py | 11 +++++++++++ .../contrib/piecewise/transform/nested_inner_repn.py | 11 +++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 49a30d0996c..ab71679aac9 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 8e8e4530d2c..e888db7eb72 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py index 009282aa310..368b92d6424 100644 --- a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index b273c9776a2..dbbd8c73bad 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, From 423dbf8b4f8ef70e9bbec5af1c2cde7fd6f207aa Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 21 Feb 2024 20:12:14 -0500 Subject: [PATCH 0505/1178] rename and cleanup, try 2 --- pyomo/contrib/piecewise/__init__.py | 4 +- .../tests/test_disaggregated_logarithmic.py | 16 +- .../transform/disaggregated_logarithmic.py | 199 ++++++++++++++++++ 3 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 8a66af89bad..b23200b3f7d 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -36,6 +36,6 @@ from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) -from pyomo.contrib.piecewise.transform.disaggreggated_logarithmic import ( - DisaggregatedLogarithmicInnerMIPTransformation, +from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import ( + DisaggregatedLogarithmicMIPTransformation, ) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index ab71679aac9..2e0fa886361 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -69,10 +69,10 @@ def check_pw_log(self, m): log_block.set_substitute.expr, log_block.substitute_var == log_block.lambdas[0, 0] * m.f1(1) - + log_block.lambdas[0, 1] * m.f1(3) + log_block.lambdas[1, 0] * m.f2(3) - + log_block.lambdas[1, 1] * m.f2(6) + log_block.lambdas[2, 0] * m.f3(6) + + log_block.lambdas[0, 1] * m.f1(3) + + log_block.lambdas[1, 1] * m.f2(6) + log_block.lambdas[2, 1] * m.f3(10), places=7, ) @@ -188,16 +188,16 @@ def check_pw_paraboloid(self, m): paraboloid_block.set_substitute.expr, paraboloid_block.substitute_var == paraboloid_block.lambdas[0, 0] * m.g1(0, 1) - + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) - + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + paraboloid_block.lambdas[1, 0] * m.g1(0, 1) - + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) - + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + paraboloid_block.lambdas[2, 0] * m.g2(3, 4) - + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) - + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + paraboloid_block.lambdas[3, 0] * m.g2(0, 7) + + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) + + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) + + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) + paraboloid_block.lambdas[3, 1] * m.g2(0, 4) + + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + paraboloid_block.lambdas[3, 2] * m.g2(3, 4), places=7, ) diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py new file mode 100644 index 00000000000..edb1a03afe6 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -0,0 +1,199 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, +) +from pyomo.core import Constraint, Binary, Var, RangeSet, Set +from pyomo.core.base import TransformationFactory +from pyomo.common.errors import DeveloperError +from math import ceil, log2 + + +@TransformationFactory.register( + "contrib.piecewise.disaggregated_logarithmic", + doc=""" + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. + """, +) +class DisaggregatedLogarithmicMIPTransformation(PiecewiseLinearTransformationBase): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables, following the "disaggregated logarithmic" + method from [1]. This is a direct-to-MIP transformation; GDP is not used. + This method of logarithmically formulating the piecewise linear function + imposes no restrictions on the family of polytopes, but we assume we have + simplices in this code. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. + """ + + CONFIG = PiecewiseLinearTransformationBase.CONFIG() + _transformation_name = "pw_linear_disaggregated_log" + + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + # Get a new Block for our transformation in transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # Dimensionality of the PWLF + dimension = pw_expr.nargs() + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + + # Bounds for the substitute_var that we will widen + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # Assumption: the simplices are really full-dimensional simplices and all have the + # same number of points, which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + + # Enumeration of simplices: map from simplex number to simplex object + idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list( + zip(transBlock.simplex_indices, pw_linear_func._linear_functions) + ) + + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in simplex_indices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + if val < substitute_var_lb: + substitute_var_lb = val + if val > substitute_var_ub: + substitute_var_ub = val + transBlock.substitute_var.setlb(substitute_var_lb) + transBlock.substitute_var.setub(substitute_var_ub) + + log_dimension = ceil(log2(num_simplices)) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is enough here. + B = {} + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + + # Build up P_0 and P_plus ahead of time. + + # {P \in \mathcal{P} | B(P)_l = 0} + def P_0_init(m, l): + return [p for p in transBlock.simplex_indices if B[p][l] == 0] + + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) + + # {P \in \mathcal{P} | B(P)_l = 1} + def P_plus_init(m, l): + return [p for p in transBlock.simplex_indices if B[p][l] == 1] + + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) + + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var( + transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) + ) + + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # for Nonseparable Piecewise-Linear Optimization + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + # The branching rules, establishing using the binaries that only one simplex's lambda + # coefficients may be nonzero + # Enabling lambdas when binaries are on + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) + def simplex_choice_1(b, l): + return ( + sum( + transBlock.lambdas[P, v] + for P in transBlock.P_plus[l] + for v in transBlock.simplex_point_indices + ) + <= transBlock.binaries[l] + ) + + # Disabling lambdas when binaries are on + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) + def simplex_choice_2(b, l): + return ( + sum( + transBlock.lambdas[P, v] + for P in transBlock.P_0[l] + for v in transBlock.simplex_point_indices + ) + <= 1 - transBlock.binaries[l] + ) + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) + def x_constraint(b, i): + return pw_expr.args[i] == sum( + transBlock.lambdas[P, v] + * pw_linear_func._points[idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + for v in transBlock.simplex_point_indices + for (P, linear_func) in simplex_indices_and_lin_funcs + ) + ) + + return substitute_var + + # Not a Gray code, just a regular binary representation + # TODO test the Gray codes too + def _get_binary_vector(self, num, length): + if num != 0 and ceil(log2(num)) > length: + raise DeveloperError("Invalid input in _get_binary_vector") + # Use python's string formatting instead of bothering with modular + # arithmetic. Hopefully not slow. + return tuple(int(x) for x in format(num, f"0{length}b")) From 290be256363f2b1d98f382cfa79a8f542666f446 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 21 Feb 2024 20:18:43 -0500 Subject: [PATCH 0506/1178] redo cleanup I managed to lose --- .../piecewise/tests/test_disaggregated_logarithmic.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 2e0fa886361..f848c610e9d 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -143,17 +143,11 @@ def check_pw_paraboloid(self, m): # Substitute var self.assertIsInstance(paraboloid_block.substitute_var, Var) - # assertExpressionsEqual( - # self, - # m.indexed_c[0].expr, - # m.x >= paraboloid_block.substitute_var - # ) # Binaries self.assertIsInstance(paraboloid_block.binaries, Var) self.assertEqual(len(paraboloid_block.binaries), log_simplex_count) # Lambdas self.assertIsInstance(paraboloid_block.lambdas, Var) - # print(f"the lambdas are: {paraboloid_block.lambdas.pprint()}") self.assertEqual( len(paraboloid_block.lambdas), simplex_count * simplex_point_count ) From dd6ed3e8a94939f68644f582c1c7e1d3bb53fd18 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 21 Feb 2024 20:21:06 -0500 Subject: [PATCH 0507/1178] fix again something strange I did --- .../transform/disaggreggated_logarithmic.py | 201 ------------------ 1 file changed, 201 deletions(-) delete mode 100644 pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py deleted file mode 100644 index 368b92d6424..00000000000 --- a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py +++ /dev/null @@ -1,201 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( - PiecewiseLinearTransformationBase, -) -from pyomo.core import Constraint, Binary, Var, RangeSet, Set -from pyomo.core.base import TransformationFactory -from pyomo.common.errors import DeveloperError -from math import ceil, log2 - - -@TransformationFactory.register( - "contrib.piecewise.disaggregated_logarithmic", - doc=""" - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. - """, -) -class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): - """ - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables, following the "disaggregated logarithmic" - method from [1]. This is a direct-to-MIP transformation; GDP is not used. - This method of logarithmically formulating the piecewise linear function - imposes no restrictions on the family of polytopes, but we assume we have - simplices in this code. - - References - ---------- - [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models - for nonseparable piecewise-linear optimization: unifying framework - and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, - 2010. - """ - - CONFIG = PiecewiseLinearTransformationBase.CONFIG() - _transformation_name = "pw_linear_disaggregated_log" - - # Implement to use PiecewiseLinearTransformationBase. This function returns the Var - # that replaces the transformed piecewise linear expr - def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block for our transformation in transformation_block.transformed_functions, - # which is a Block(Any). This is where we will put our new components. - transBlock = transformation_block.transformed_functions[ - len(transformation_block.transformed_functions) - ] - - # Dimensionality of the PWLF - dimension = pw_expr.nargs() - transBlock.dimension_indices = RangeSet(0, dimension - 1) - - # Substitute Var that will hold the value of the PWLE - substitute_var = transBlock.substitute_var = Var() - pw_linear_func.map_transformation_var(pw_expr, substitute_var) - - # Bounds for the substitute_var that we will widen - substitute_var_lb = float("inf") - substitute_var_ub = -float("inf") - - # Simplices are tuples of indices of points. Give them their own indices, too - simplices = pw_linear_func._simplices - num_simplices = len(simplices) - transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really full-dimensional simplices and all have the - # same number of points, which is dimension + 1 - transBlock.simplex_point_indices = RangeSet(0, dimension) - - # Enumeration of simplices: map from simplex number to simplex object - idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - - # List of tuples of simplex indices with their linear function - simplex_indices_and_lin_funcs = list( - zip(transBlock.simplex_indices, pw_linear_func._linear_functions) - ) - - # We don't seem to get a convenient opportunity later, so let's just widen - # the bounds here. All we need to do is go through the corners of each simplex. - for P, linear_func in simplex_indices_and_lin_funcs: - for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) - if val < substitute_var_lb: - substitute_var_lb = val - if val > substitute_var_ub: - substitute_var_ub = val - transBlock.substitute_var.setlb(substitute_var_lb) - transBlock.substitute_var.setub(substitute_var_ub) - - log_dimension = ceil(log2(num_simplices)) - transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - - # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors. Any injective function - # is enough here. - B = {} - for i in transBlock.simplex_indices: - # map index(P) -> corresponding vector in {0, 1}^n - B[i] = self._get_binary_vector(i, log_dimension) - - # Build up P_0 and P_plus ahead of time. - - # {P \in \mathcal{P} | B(P)_l = 0} - def P_0_init(m, l): - return [p for p in transBlock.simplex_indices if B[p][l] == 0] - - transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) - - # {P \in \mathcal{P} | B(P)_l = 1} - def P_plus_init(m, l): - return [p for p in transBlock.simplex_indices if B[p][l] == 1] - - transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) - - # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it - transBlock.lambdas = Var( - transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) - ) - - # Numbered citations are from Vielma et al 2010, Mixed-Integer Models - # for Nonseparable Piecewise-Linear Optimization - - # Sum of all lambdas is one (6b) - transBlock.convex_combo = Constraint( - expr=sum( - transBlock.lambdas[P, v] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - == 1 - ) - - # The branching rules, establishing using the binaries that only one simplex's lambda - # coefficients may be nonzero - # Enabling lambdas when binaries are on - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) - def simplex_choice_1(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in transBlock.P_plus[l] - for v in transBlock.simplex_point_indices - ) - <= transBlock.binaries[l] - ) - - # Disabling lambdas when binaries are on - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) - def simplex_choice_2(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in transBlock.P_0[l] - for v in transBlock.simplex_point_indices - ) - <= 1 - transBlock.binaries[l] - ) - - # for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) - @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) - def x_constraint(b, i): - return pw_expr.args[i] == sum( - transBlock.lambdas[P, v] - * pw_linear_func._points[idx_to_simplex[P][v]][i] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - - # Make the substitute Var equal the PWLE (6a.2) - transBlock.set_substitute = Constraint( - expr=substitute_var - == sum( - sum( - transBlock.lambdas[P, v] - * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) - for v in transBlock.simplex_point_indices - ) - for (P, linear_func) in simplex_indices_and_lin_funcs - ) - ) - - return substitute_var - - # Not a Gray code, just a regular binary representation - # TODO test the Gray codes too - def _get_binary_vector(self, num, length): - if num != 0 and ceil(log2(num)) > length: - raise DeveloperError("Invalid input in _get_binary_vector") - # Use python's string formatting instead of bothering with modular - # arithmetic. Hopefully not slow. - return tuple(int(x) for x in format(num, f"0{length}b")) From 289915dcc11371a3f423b27adfb055824029b52b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 22 Feb 2024 14:40:11 -0500 Subject: [PATCH 0508/1178] move duplicated methods into common file for inner-repn-GDP based transforms --- .../tests/common_inner_repn_tests.py | 80 ++++++++++++++++++ .../piecewise/tests/test_inner_repn_gdp.py | 70 ++-------------- .../tests/test_nested_inner_repn_gdp.py | 82 ++----------------- 3 files changed, 95 insertions(+), 137 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/common_inner_repn_tests.py diff --git a/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py b/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py new file mode 100644 index 00000000000..e0b8e878be3 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py @@ -0,0 +1,80 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core import Var +from pyomo.core.base import Constraint +from pyomo.core.expr.compare import assertExpressionsEqual + +# This file contains check methods shared between GDP inner representation-based +# transformations. Currently, those are the inner_representation_gdp and +# nested_inner_repn_gdp transformations, since each have disjuncts with the +# same structure. + + +# Check one disjunct from the log model for proper contents +def check_log_disjunct(test, d, pts, f, substitute_var, x): + test.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + test.assertEqual(len(d.component_map(Var)), 2) + test.assertIsInstance(d.lambdas, Var) + test.assertEqual(len(d.lambdas), 2) + for lamb in d.lambdas.values(): + test.assertEqual(lamb.lb, 0) + test.assertEqual(lamb.ub, 1) + test.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual(test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1) + test.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + test, d.set_substitute.expr, substitute_var == f(x), places=7 + ) + test.assertIsInstance(d.linear_combo, Constraint) + test.assertEqual(len(d.linear_combo), 1) + assertExpressionsEqual( + test, d.linear_combo[0].expr, x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1] + ) + + +# Check one disjunct from the paraboloid model for proper contents. +def check_paraboloid_disjunct(test, d, pts, f, substitute_var, x1, x2): + test.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + test.assertEqual(len(d.component_map(Var)), 2) + test.assertIsInstance(d.lambdas, Var) + test.assertEqual(len(d.lambdas), 3) + for lamb in d.lambdas.values(): + test.assertEqual(lamb.lb, 0) + test.assertEqual(lamb.ub, 1) + test.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 + ) + test.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + test, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 + ) + test.assertIsInstance(d.linear_combo, Constraint) + test.assertEqual(len(d.linear_combo), 2) + assertExpressionsEqual( + test, + d.linear_combo[0].expr, + x1 + == pts[0][0] * d.lambdas[0] + + pts[1][0] * d.lambdas[1] + + pts[2][0] * d.lambdas[2], + ) + assertExpressionsEqual( + test, + d.linear_combo[1].expr, + x2 + == pts[0][1] * d.lambdas[0] + + pts[1][1] * d.lambdas[1] + + pts[2][1] * d.lambdas[2], + ) diff --git a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py index 27fe43e54d5..e7505bb92d3 100644 --- a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +import pyomo.contrib.piecewise.tests.common_inner_repn_tests as inner_repn_tests from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import ( assertExpressionsEqual, @@ -22,67 +23,6 @@ class TestTransformPiecewiseModelToInnerRepnGDP(unittest.TestCase): - def check_log_disjunct(self, d, pts, f, substitute_var, x): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 2) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 1) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], - ) - - def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 3) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 2) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x1 - == pts[0][0] * d.lambdas[0] - + pts[1][0] * d.lambdas[1] - + pts[2][0] * d.lambdas[2], - ) - assertExpressionsEqual( - self, - d.linear_combo[1].expr, - x2 - == pts[0][1] * d.lambdas[0] - + pts[1][1] * d.lambdas[1] - + pts[2][1] * d.lambdas[2], - ) - def check_pw_log(self, m): ## # Check the transformation of the approximation of log(x) @@ -101,7 +41,9 @@ def check_pw_log(self, m): log_block.disjuncts[2]: ((6, 10), m.f3), } for d, (pts, f) in disjuncts_dict.items(): - self.check_log_disjunct(d, pts, f, log_block.substitute_var, m.x) + inner_repn_tests.check_log_disjunct( + self, d, pts, f, log_block.substitute_var, m.x + ) # Check the Disjunction self.assertIsInstance(log_block.pick_a_piece, Disjunction) @@ -129,8 +71,8 @@ def check_pw_paraboloid(self, m): paraboloid_block.disjuncts[3]: ([(0, 7), (0, 4), (3, 4)], m.g2), } for d, (pts, f) in disjuncts_dict.items(): - self.check_paraboloid_disjunct( - d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + inner_repn_tests.check_paraboloid_disjunct( + self, d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 ) # Check the Disjunction diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index e888db7eb72..2024f014f55 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +import pyomo.contrib.piecewise.tests.common_inner_repn_tests as inner_repn_tests from pyomo.core.base import TransformationFactory from pyomo.environ import SolverFactory, Var, Constraint from pyomo.gdp import Disjunction, Disjunct @@ -20,71 +21,6 @@ # Test the nested inner repn gdp model using the common_tests code class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): - # Check one disjunct for proper contents. Disjunct structure should be - # identical to the version for the inner representation gdp - def check_log_disjunct(self, d, pts, f, substitute_var, x): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 2) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 1) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], - ) - - # Check one disjunct from the paraboloid block for proper contents. This should - # be identical to the inner_representation_gdp one - def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 3) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 2) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x1 - == pts[0][0] * d.lambdas[0] - + pts[1][0] * d.lambdas[1] - + pts[2][0] * d.lambdas[2], - ) - assertExpressionsEqual( - self, - d.linear_combo[1].expr, - x2 - == pts[0][1] * d.lambdas[0] - + pts[1][1] * d.lambdas[1] - + pts[2][1] * d.lambdas[2], - ) - # Check the structure of the log PWLF Block def check_pw_log(self, m): z = m.pw_log.get_transformation_var(m.log_expr) @@ -110,8 +46,8 @@ def check_pw_log(self, m): # Left disjunct with constraints self.assertIsInstance(log_block.d_l, Disjunct) - self.check_log_disjunct( - log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x + inner_repn_tests.check_log_disjunct( + self, log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x ) # Right disjunct with disjunction @@ -121,12 +57,12 @@ def check_pw_log(self, m): # Left and right child disjuncts with constraints self.assertIsInstance(log_block.d_r.d_l, Disjunct) - self.check_log_disjunct( - log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x + inner_repn_tests.check_log_disjunct( + self, log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x ) self.assertIsInstance(log_block.d_r.d_r, Disjunct) - self.check_log_disjunct( - log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x + inner_repn_tests.check_log_disjunct( + self, log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x ) # Check that this also became the objective @@ -155,8 +91,8 @@ def check_pw_paraboloid(self, m): paraboloid_block.d_r.d_r: ([(0, 7), (0, 4), (3, 4)], m.g2), } for d, (pts, f) in disjuncts_dict.items(): - self.check_paraboloid_disjunct( - d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + inner_repn_tests.check_paraboloid_disjunct( + self, d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 ) # And check the substitute Var is in the objective now. From fcddaf5a0a10503a73ecd1c30009eaefca6cee86 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:02:57 -0700 Subject: [PATCH 0509/1178] Creating a GDP-wide private data class and converting hull to use it for constraint mappings --- .../gdp/plugins/gdp_to_mip_transformation.py | 11 ++++ pyomo/gdp/plugins/hull.py | 53 ++++++++----------- pyomo/gdp/util.py | 8 +-- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 59cb221321a..7cc55b80f78 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -11,6 +11,7 @@ from functools import wraps +from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name @@ -48,6 +49,16 @@ from weakref import ref as weakref_ref +class _GDPTransformationData(AutoSlots.Mixin): + __slots__ = ('src_constraint', 'transformed_constraint') + def __init__(self): + self.src_constraint = ComponentMap() + self.transformed_constraint = ComponentMap() + + +Block.register_private_data_initializer(_GDPTransformationData, scope='pyomo.gdp') + + class GDP_to_MIP_Transformation(Transformation): """ Base class for transformations from GDP to MIP diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index d1c38bde039..b4e9fccc089 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -95,20 +95,11 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): The transformation will create a new Block with a unique name beginning "_pyomo_gdp_hull_reformulation". It will contain an indexed Block named "relaxedDisjuncts" that will hold the relaxed - disjuncts. This block is indexed by an integer indicating the order - in which the disjuncts were relaxed. Each block has a dictionary - "_constraintMap": - - 'srcConstraints': ComponentMap(: - ), - 'transformedConstraints': - ComponentMap( : - , - : []) - - All transformed Disjuncts will have a pointer to the block their transformed - constraints are on, and all transformed Disjunctions will have a - pointer to the corresponding OR or XOR constraint. + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. All transformed Disjuncts will + have a pointer to the block their transformed constraints are on, + and all transformed Disjunctions will have a pointer to the + corresponding OR or XOR constraint. The _pyomo_gdp_hull_reformulation block will have a ComponentMap "_disaggregationConstraintMap": @@ -675,7 +666,7 @@ def _transform_constraint( ): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() - constraintMap = relaxationBlock._constraintMap + constraint_map = relaxationBlock.private_data('pyomo.gdp') # We will make indexes from ({obj.local_name} x obj.index_set() x ['lb', # 'ub']), but don't bother construct that set here, as taking Cartesian @@ -757,19 +748,19 @@ def _transform_constraint( # this variable, so I'm going to return # it. Alternatively we could return an empty list, but I # think I like this better. - constraintMap['transformedConstraints'][c] = [v[0]] + constraint_map.transformed_constraint[c] = [v[0]] # Reverse map also (this is strange) - constraintMap['srcConstraints'][v[0]] = c + constraint_map.src_constraint[v[0]] = c continue newConsExpr = expr - (1 - y) * h_0 == c.lower * y if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'eq'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c + constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) # map to the _ConstraintData (And yes, for @@ -779,10 +770,10 @@ def _transform_constraint( # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the # _ConstraintDatas to each other above) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, 'eq'] ] - constraintMap['srcConstraints'][newConstraint[name, 'eq']] = c + constraint_map.src_constraint[newConstraint[name, 'eq']] = c continue @@ -797,16 +788,16 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'lb'), newConsExpr) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'lb'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c else: newConstraint.add((name, 'lb'), newConsExpr) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, 'lb'] ] - constraintMap['srcConstraints'][newConstraint[name, 'lb']] = c + constraint_map.src_constraint[newConstraint[name, 'lb']] = c if c.upper is not None: if self._generate_debug_messages: @@ -821,24 +812,24 @@ def _transform_constraint( newConstraint.add((name, i, 'ub'), newConsExpr) # map (have to account for fact we might have created list # above - transformed = constraintMap['transformedConstraints'].get(c) + transformed = constraint_map.transformed_constraint.get(c) if transformed is not None: transformed.append(newConstraint[name, i, 'ub']) else: - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'ub'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c else: newConstraint.add((name, 'ub'), newConsExpr) - transformed = constraintMap['transformedConstraints'].get(c) + transformed = constraint_map.transformed_constraint.get(c) if transformed is not None: transformed.append(newConstraint[name, 'ub']) else: - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, 'ub'] ] - constraintMap['srcConstraints'][newConstraint[name, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, 'ub']] = c # deactivate now that we have transformed obj.deactivate() diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 57eef29eded..9f929fbc621 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -477,16 +477,17 @@ def get_src_constraint(transformedConstraint): a transformation block """ transBlock = transformedConstraint.parent_block() + src_constraints = transBlock.private_data('pyomo.gdp').src_constraint # This should be our block, so if it's not, the user messed up and gave # us the wrong thing. If they happen to also have a _constraintMap then # the world is really against us. - if not hasattr(transBlock, "_constraintMap"): + if transformedConstraint not in src_constraints: raise GDP_Error( "Constraint '%s' is not a transformed constraint" % transformedConstraint.name ) # if something goes wrong here, it's a bug in the mappings. - return transBlock._constraintMap['srcConstraints'][transformedConstraint] + return src_constraints[transformedConstraint] def _find_parent_disjunct(constraint): @@ -538,7 +539,8 @@ def get_transformed_constraints(srcConstraint): ) transBlock = _get_constraint_transBlock(srcConstraint) try: - return transBlock._constraintMap['transformedConstraints'][srcConstraint] + return transBlock.private_data('pyomo.gdp').transformed_constraint[ + srcConstraint] except: logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) raise From 735056b9a8e90b5d0efd4b981e84dcdaa022267e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:18:43 -0700 Subject: [PATCH 0510/1178] Moving bigm transformations onto private data for mapping constraints --- pyomo/gdp/plugins/bigm.py | 15 ++----- pyomo/gdp/plugins/bigm_mixin.py | 14 +++---- .../gdp/plugins/gdp_to_mip_transformation.py | 7 ---- pyomo/gdp/plugins/multiple_bigm.py | 40 +++++++++---------- 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 1f9f561b192..ce98180e9d0 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -94,15 +94,8 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): name beginning "_pyomo_gdp_bigm_reformulation". That Block will contain an indexed Block named "relaxedDisjuncts", which will hold the relaxed disjuncts. This block is indexed by an integer - indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": - - 'srcConstraints': ComponentMap(: - ) - 'transformedConstraints': ComponentMap(: - ) - - All transformed Disjuncts will have a pointer to the block their transformed + indicating the order in which the disjuncts were relaxed. All + transformed Disjuncts will have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding 'Or' or 'ExactlyOne' constraint. @@ -279,7 +272,7 @@ def _transform_constraint( # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src - constraintMap = transBlock._constraintMap + constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -346,7 +339,7 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraintMap + c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map ) # deactivate because we relaxed diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index ad6e6dcad86..59d79331a34 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -232,7 +232,7 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraintMap + self, c, i, M, indicator_var, newConstraint, constraint_map ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -253,8 +253,8 @@ def _add_constraint_expressions( ) M_expr = M[0] * (1 - indicator_var) newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) - constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'lb']] - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.transformed_constraint[c] = [newConstraint[name, i, 'lb']] + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: raise GDP_Error( @@ -263,13 +263,13 @@ def _add_constraint_expressions( ) M_expr = M[1] * (1 - indicator_var) newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) - transformed = constraintMap['transformedConstraints'].get(c) + transformed = constraint_map.transformed_constraint.get(c) if transformed is not None: - constraintMap['transformedConstraints'][c].append( + constraint_map.transformed_constraint[c].append( newConstraint[name, i, 'ub'] ) else: - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'ub'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 7cc55b80f78..9547d80bab3 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -254,14 +254,7 @@ def _get_disjunct_transformation_block(self, disjunct, transBlock): relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] relaxationBlock.transformedConstraints = Constraint(Any) - relaxationBlock.localVarReferences = Block() - # add the map that will link back and forth between transformed - # constraints and their originals. - relaxationBlock._constraintMap = { - 'srcConstraints': ComponentMap(), - 'transformedConstraints': ComponentMap(), - } # add mappings to source disjunct (so we'll know we've relaxed) disjunct._transformation_block = weakref_ref(relaxationBlock) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 6177de3c037..3d867798161 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -359,7 +359,7 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() - constraintMap = relaxationBlock._constraintMap + constraint_map = relaxationBlock.private_data('pyomo.gdp') transBlock = relaxationBlock.parent_block() # Though rare, it is possible to get naming conflicts here @@ -397,8 +397,8 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): newConstraint.add((i, 'ub'), c.body - c.upper <= rhs) transformed.append(newConstraint[i, 'ub']) for c_new in transformed: - constraintMap['srcConstraints'][c_new] = [c] - constraintMap['transformedConstraints'][c] = transformed + constraint_map.src_constraint[c_new] = [c] + constraint_map.transformed_constraint[c] = transformed else: lower = (None, None, None) upper = (None, None, None) @@ -427,7 +427,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): M, disjunct.indicator_var.get_associated_binary(), newConstraint, - constraintMap, + constraint_map, ) # deactivate now that we have transformed @@ -496,6 +496,7 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): relaxationBlock = self._get_disjunct_transformation_block( disj, transBlock ) + constraint_map = relaxationBlock.private_data('pyomo.gdp') if len(lower_dict) > 0: M = lower_dict.get(disj, None) if M is None: @@ -527,39 +528,36 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): idx = i + offset if len(lower_dict) > 0: transformed.add((idx, 'lb'), v >= lower_rhs) - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'lb'] ] = [] for c, disj in lower_bound_constraints_by_var[v]: - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'lb'] ].append(c) - disj.transformation_block._constraintMap['transformedConstraints'][ + disj.transformation_block.private_data( + 'pyomo.gdp').transformed_constraint[ c ] = [transformed[idx, 'lb']] if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'ub'] ] = [] for c, disj in upper_bound_constraints_by_var[v]: - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'ub'] ].append(c) # might already be here if it had an upper bound - if ( - c - in disj.transformation_block._constraintMap[ - 'transformedConstraints' - ] - ): - disj.transformation_block._constraintMap[ - 'transformedConstraints' - ][c].append(transformed[idx, 'ub']) + disj_constraint_map = disj.transformation_block.private_data( + 'pyomo.gdp') + if c in disj_constraint_map.transformed_constraint: + disj_constraint_map.transformed_constraint[c].append( + transformed[idx, 'ub']) else: - disj.transformation_block._constraintMap[ - 'transformedConstraints' - ][c] = [transformed[idx, 'ub']] + disj_constraint_map.transformed_constraint[c] = [ + transformed[idx, 'ub'] + ] return transformed_constraints From 763586e44b6081f800094d954bdf5fe6a0f4105c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:21:44 -0700 Subject: [PATCH 0511/1178] Moving binary multiplication onto private data mappings --- pyomo/gdp/plugins/binary_multiplication.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index d68f7efe76f..6d0955c95a7 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -121,7 +121,7 @@ def _transform_disjunct(self, obj, transBlock): def _transform_constraint(self, obj, disjunct): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() - constraintMap = transBlock._constraintMap + constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -137,14 +137,14 @@ def _transform_constraint(self, obj, disjunct): continue self._add_constraint_expressions( - c, i, disjunct.binary_indicator_var, newConstraint, constraintMap + c, i, disjunct.binary_indicator_var, newConstraint, constraint_map ) # deactivate because we relaxed c.deactivate() def _add_constraint_expressions( - self, c, i, indicator_var, newConstraint, constraintMap + self, c, i, indicator_var, newConstraint, constraint_map ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -156,21 +156,21 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique - transformed = constraintMap['transformedConstraints'][c] = [] + transformed = constraint_map.transformed_constraint[c] = [] lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: # equality newConstraint.add((name, i, 'eq'), (c.body - lb) * indicator_var == 0) transformed.append(newConstraint[name, i, 'eq']) - constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c + constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: # inequality if lb is not None: newConstraint.add((name, i, 'lb'), 0 <= (c.body - lb) * indicator_var) transformed.append(newConstraint[name, i, 'lb']) - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if ub is not None: newConstraint.add((name, i, 'ub'), (c.body - ub) * indicator_var <= 0) transformed.append(newConstraint[name, i, 'ub']) - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c From b0da69ce9a6ae69aae3cb07ef38fcd890ecefcae Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:22:59 -0700 Subject: [PATCH 0512/1178] Even black thinks this is prettier --- .../gdp/plugins/gdp_to_mip_transformation.py | 1 + pyomo/gdp/plugins/multiple_bigm.py | 27 +++++++------------ pyomo/gdp/util.py | 3 ++- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 9547d80bab3..94dde433a15 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -51,6 +51,7 @@ class _GDPTransformationData(AutoSlots.Mixin): __slots__ = ('src_constraint', 'transformed_constraint') + def __init__(self): self.src_constraint = ComponentMap() self.transformed_constraint = ComponentMap() diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 3d867798161..fccb0514dfb 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -528,32 +528,25 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): idx = i + offset if len(lower_dict) > 0: transformed.add((idx, 'lb'), v >= lower_rhs) - constraint_map.src_constraint[ - transformed[idx, 'lb'] - ] = [] + constraint_map.src_constraint[transformed[idx, 'lb']] = [] for c, disj in lower_bound_constraints_by_var[v]: - constraint_map.src_constraint[ - transformed[idx, 'lb'] - ].append(c) + constraint_map.src_constraint[transformed[idx, 'lb']].append(c) disj.transformation_block.private_data( - 'pyomo.gdp').transformed_constraint[ - c - ] = [transformed[idx, 'lb']] + 'pyomo.gdp' + ).transformed_constraint[c] = [transformed[idx, 'lb']] if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) - constraint_map.src_constraint[ - transformed[idx, 'ub'] - ] = [] + constraint_map.src_constraint[transformed[idx, 'ub']] = [] for c, disj in upper_bound_constraints_by_var[v]: - constraint_map.src_constraint[ - transformed[idx, 'ub'] - ].append(c) + constraint_map.src_constraint[transformed[idx, 'ub']].append(c) # might already be here if it had an upper bound disj_constraint_map = disj.transformation_block.private_data( - 'pyomo.gdp') + 'pyomo.gdp' + ) if c in disj_constraint_map.transformed_constraint: disj_constraint_map.transformed_constraint[c].append( - transformed[idx, 'ub']) + transformed[idx, 'ub'] + ) else: disj_constraint_map.transformed_constraint[c] = [ transformed[idx, 'ub'] diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 9f929fbc621..b3e7de8a7cd 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -540,7 +540,8 @@ def get_transformed_constraints(srcConstraint): transBlock = _get_constraint_transBlock(srcConstraint) try: return transBlock.private_data('pyomo.gdp').transformed_constraint[ - srcConstraint] + srcConstraint + ] except: logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) raise From e7acc12dbefa4a2d53305cd2b517bfeaac5222a0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:30:46 -0700 Subject: [PATCH 0513/1178] fix division by zero error in linear presolve --- pyomo/contrib/solver/ipopt.py | 44 ++++++++----- .../solver/tests/solvers/test_solvers.py | 65 +++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 3ac1a5ac4a2..dc632adb184 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -17,7 +17,11 @@ from pyomo.common import Executable from pyomo.common.config import ConfigValue, document_kwargs_from_configdict, ConfigDict -from pyomo.common.errors import PyomoException, DeveloperError +from pyomo.common.errors import ( + PyomoException, + DeveloperError, + InfeasibleConstraintException, +) from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.var import _GeneralVarData @@ -72,11 +76,7 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', - ConfigValue( - default=NLWriter.CONFIG(), - description="Configuration that controls options in the NL writer.", - ), + 'writer_config', NLWriter.CONFIG() ) @@ -314,15 +314,19 @@ def solve(self, model, **kwds): ) as row_file, open(basename + '.col', 'w') as col_file: timer.start('write_nl_file') self._writer.config.set_value(config.writer_config) - nl_info = self._writer.write( - model, - nl_file, - row_file, - col_file, - symbolic_solver_labels=config.symbolic_solver_labels, - ) + try: + nl_info = self._writer.write( + model, + nl_file, + row_file, + col_file, + symbolic_solver_labels=config.symbolic_solver_labels, + ) + proven_infeasible = False + except InfeasibleConstraintException: + proven_infeasible = True timer.stop('write_nl_file') - if len(nl_info.variables) > 0: + if not proven_infeasible and len(nl_info.variables) > 0: # Get a copy of the environment to pass to the subprocess env = os.environ.copy() if nl_info.external_function_libraries: @@ -361,11 +365,17 @@ def solve(self, model, **kwds): timer.stop('subprocess') # This is the stuff we need to parse to get the iterations # and time - iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( + (iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time) = ( self._parse_ipopt_output(ostreams[0]) ) - if len(nl_info.variables) == 0: + if proven_infeasible: + results = Results() + results.termination_condition = TerminationCondition.provenInfeasible + results.solution_loader = SolSolutionLoader(None, None) + results.iteration_count = 0 + results.timing_info.total_seconds = 0 + elif len(nl_info.variables) == 0: if len(nl_info.eliminated_vars) == 0: results = Results() results.termination_condition = TerminationCondition.emptyModel @@ -457,7 +467,7 @@ def solve(self, model, **kwds): ) results.solver_configuration = config - if len(nl_info.variables) > 0: + if not proven_infeasible and len(nl_info.variables) > 0: results.solver_log = ostreams[0].getvalue() # Capture/record end-time / wall-time diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index cf5f6cf5c57..a4f4a3bc389 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1508,6 +1508,71 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool) res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, -18, 5) + @parameterized.expand(input=_load_tests(nl_solvers)) + def test_presolve_with_zero_coef( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + """ + when c2 gets presolved out, c1 becomes + x - y + y = 0 which becomes + x - 0*y == 0 which is the zero we are testing for + """ + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = pe.Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = pe.Constraint(expr=m.z == -m.y) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2.25) + self.assertAlmostEqual(m.x.value, 1.5) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + m.x.setlb(2) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + if use_presolve: + exp = TerminationCondition.provenInfeasible + else: + exp = TerminationCondition.locallyInfeasible + self.assertEqual(res.termination_condition, exp) + + m = pe.ConcreteModel() + m.w = pe.Var() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2 + m.w**2) + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z) + m.c2 = pe.Constraint(expr=m.z == -m.y) + m.c3 = pe.Constraint(expr=m.x == -m.w) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(m.w.value, 0) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + del m.c1 + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z + 1.5) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + self.assertEqual(res.termination_condition, exp) + @parameterized.expand(input=_load_tests(all_solvers)) def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() From 0dabe3fe9d0636e9122544dbdcec16d61907bd84 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:35:17 -0700 Subject: [PATCH 0514/1178] fix division by zero error in linear presolve --- pyomo/repn/plugins/nl_writer.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index f3ff94ea8c9..66c695dafa3 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1820,10 +1820,24 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # appropriately (that expr_info is persisting in the # eliminated_vars dict - and we will use that to # update other linear expressions later.) + old_nnz = len(expr_info.linear) c = expr_info.linear.pop(_id, 0) + nnz = old_nnz - 1 expr_info.const += c * b if x in expr_info.linear: expr_info.linear[x] += c * a + if expr_info.linear[x] == 0: + nnz -= 1 + coef = expr_info.linear.pop(x) + if not nnz: + if abs(expr_info.const) > TOL: + # constraint is trivially infeasible + raise InfeasibleConstraintException( + "model contains a trivially infeasible constrint " + f"{expr_info.const} == {coef}*{var_map[x]}" + ) + # constraint is trivially feasible + eliminated_cons.add(con_id) elif a: expr_info.linear[x] = c * a # replacing _id with x... NNZ is not changing, @@ -1831,9 +1845,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # this constraint comp_by_linear_var[x].append((con_id, expr_info)) continue - # NNZ has been reduced by 1 - nnz = len(expr_info.linear) - _old = lcon_by_linear_nnz[nnz + 1] + _old = lcon_by_linear_nnz[old_nnz] if con_id in _old: lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id) # If variables were replaced by the variable that From b59978e8b8fba623e4affa00ba94713b006af32f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:35:25 -0700 Subject: [PATCH 0515/1178] Moving hull disaggregation constraint mappings to private_data --- pyomo/gdp/plugins/hull.py | 27 +++++++-------------------- pyomo/gdp/tests/test_hull.py | 3 +-- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b4e9fccc089..4a7445283a9 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -58,12 +58,14 @@ class _HullTransformationData(AutoSlots.Mixin): - __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map') + __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map', + 'disaggregation_constraint_map') def __init__(self): self.disaggregated_var_map = DefaultComponentMap(ComponentMap) self.original_var_map = ComponentMap() self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + self.disaggregation_constraint_map = DefaultComponentMap(ComponentMap) Block.register_private_data_initializer(_HullTransformationData) @@ -100,10 +102,6 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding OR or XOR constraint. - - The _pyomo_gdp_hull_reformulation block will have a ComponentMap - "_disaggregationConstraintMap": - :ComponentMap(: ) """ CONFIG = cfg.ConfigDict('gdp.hull') @@ -285,10 +283,6 @@ def _add_transformation_block(self, to_block): # Disjunctions we transform onto this block here. transBlock.disaggregationConstraints = Constraint(NonNegativeIntegers) - # This will map from srcVar to a map of srcDisjunction to the - # disaggregation constraint corresponding to srcDisjunction - transBlock._disaggregationConstraintMap = ComponentMap() - # we are going to store some of the disaggregated vars directly here # when we have vars that don't appear in every disjunct transBlock._disaggregatedVars = Var(NonNegativeIntegers, dense=False) @@ -321,7 +315,7 @@ def _transform_disjunctionData( ) disaggregationConstraint = transBlock.disaggregationConstraints - disaggregationConstraintMap = transBlock._disaggregationConstraintMap + disaggregationConstraintMap = transBlock.private_data().disaggregation_constraint_map disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints @@ -490,13 +484,7 @@ def _transform_disjunctionData( # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction - if var in disaggregationConstraintMap: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + disaggregationConstraintMap[var][obj] = disaggregationConstraint[cons_idx] # deactivate for the writers obj.deactivate() @@ -922,9 +910,8 @@ def get_disaggregation_constraint( ) try: - cons = transBlock.parent_block()._disaggregationConstraintMap[original_var][ - disjunction - ] + cons = transBlock.parent_block().private_data().disaggregation_constraint_map[ + original_var][disjunction] except: if raise_exception: logger.error( diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 858764759ee..98322d4888d 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -2314,8 +2314,7 @@ def test_mapping_method_errors(self): with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): self.assertRaisesRegex( KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", + r".*disjunction", hull.get_disaggregation_constraint, m.d[1].transformation_block.disaggregatedVars.w, m.disjunction, From 9f3d6980eaff3b996bf434da2f862d07ce4fd53e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:36:01 -0700 Subject: [PATCH 0516/1178] black adding newlines --- pyomo/gdp/plugins/hull.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 4a7445283a9..a2b050298e2 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -58,8 +58,12 @@ class _HullTransformationData(AutoSlots.Mixin): - __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map', - 'disaggregation_constraint_map') + __slots__ = ( + 'disaggregated_var_map', + 'original_var_map', + 'bigm_constraint_map', + 'disaggregation_constraint_map', + ) def __init__(self): self.disaggregated_var_map = DefaultComponentMap(ComponentMap) @@ -315,7 +319,9 @@ def _transform_disjunctionData( ) disaggregationConstraint = transBlock.disaggregationConstraints - disaggregationConstraintMap = transBlock.private_data().disaggregation_constraint_map + disaggregationConstraintMap = ( + transBlock.private_data().disaggregation_constraint_map + ) disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints @@ -910,8 +916,11 @@ def get_disaggregation_constraint( ) try: - cons = transBlock.parent_block().private_data().disaggregation_constraint_map[ - original_var][disjunction] + cons = ( + transBlock.parent_block() + .private_data() + .disaggregation_constraint_map[original_var][disjunction] + ) except: if raise_exception: logger.error( From 1acf6188789f63d558d521e3d8f1c8e7a99d55d8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:37:03 -0700 Subject: [PATCH 0517/1178] fix typo --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 66c695dafa3..bd7ade34923 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1760,7 +1760,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): id2_isdiscrete = var_map[id2].domain.isdiscrete() if var_map[_id].domain.isdiscrete() ^ id2_isdiscrete: # if only one variable is discrete, then we need to - # substiitute out the other + # substitute out the other if id2_isdiscrete: _id, id2 = id2, _id coef, coef2 = coef2, coef From 4cceaa99653c0bdcdec759b75e249f73a2f9723c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:42:41 -0700 Subject: [PATCH 0518/1178] fix typo --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index bd7ade34923..3fd97ac06d0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1833,7 +1833,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): if abs(expr_info.const) > TOL: # constraint is trivially infeasible raise InfeasibleConstraintException( - "model contains a trivially infeasible constrint " + "model contains a trivially infeasible constraint " f"{expr_info.const} == {coef}*{var_map[x]}" ) # constraint is trivially feasible From 6811943281374a5732d35660e1cda0c1c9ed1ca5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:45:33 -0700 Subject: [PATCH 0519/1178] Moving bigM src mapping to private data --- pyomo/gdp/plugins/bigm.py | 43 ++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index ce98180e9d0..bcf9606877b 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -13,6 +13,7 @@ import logging +from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.gc_manager import PauseGC @@ -58,6 +59,23 @@ logger = logging.getLogger('pyomo.gdp.bigm') +class _BigMData(AutoSlots.Mixin): + __slots__ = ('bigm_src',) + def __init__(self): + # we will keep a map of constraints (hashable, ha!) to a tuple to + # indicate what their M value is and where it came from, of the form: + # ((lower_value, lower_source, lower_key), (upper_value, upper_source, + # upper_key)), where the first tuple is the information for the lower M, + # the second tuple is the info for the upper M, source is the Suffix or + # argument dictionary and None if the value was calculated, and key is + # the key in the Suffix or argument dictionary, and None if it was + # calculated. (Note that it is possible the lower or upper is + # user-specified and the other is not, hence the need to store + # information for both.) + self.bigm_src = {} + +Block.register_private_data_initializer(_BigMData) + @TransformationFactory.register( 'gdp.bigm', doc="Relax disjunctive model using big-M terms." ) @@ -240,18 +258,6 @@ def _transform_disjunct(self, obj, bigM, transBlock): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) - # we will keep a map of constraints (hashable, ha!) to a tuple to - # indicate what their M value is and where it came from, of the form: - # ((lower_value, lower_source, lower_key), (upper_value, upper_source, - # upper_key)), where the first tuple is the information for the lower M, - # the second tuple is the info for the upper M, source is the Suffix or - # argument dictionary and None if the value was calculated, and key is - # the key in the Suffix or argument dictionary, and None if it was - # calculated. (Note that it is possible the lower or upper is - # user-specified and the other is not, hence the need to store - # information for both.) - relaxationBlock.bigm_src = {} - # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big # deal for Hull, as it uses the component_objects / @@ -271,7 +277,7 @@ def _transform_constraint( ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() - bigm_src = transBlock.bigm_src + bigm_src = transBlock.private_data().bigm_src constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -402,7 +408,7 @@ def _update_M_from_suffixes(self, constraint, suffix_list, lower, upper): def get_m_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) ((lower_val, lower_source, lower_key), (upper_val, upper_source, upper_key)) = ( - transBlock.bigm_src[constraint] + transBlock.private_data().bigm_src[constraint] ) if ( @@ -457,7 +463,7 @@ def get_M_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) - return transBlock.bigm_src[constraint] + return transBlock.private_data().bigm_src[constraint] def get_M_value(self, constraint): """Returns the M values used to transform constraint. Return is a tuple: @@ -472,7 +478,7 @@ def get_M_value(self, constraint): transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) - lower, upper = transBlock.bigm_src[constraint] + lower, upper = transBlock.private_data().bigm_src[constraint] return (lower[0], upper[0]) def get_all_M_values_by_constraint(self, model): @@ -492,9 +498,8 @@ def get_all_M_values_by_constraint(self, model): # First check if it was transformed at all. if transBlock is not None: # If it was transformed with BigM, we get the M values. - if hasattr(transBlock, 'bigm_src'): - for cons in transBlock.bigm_src: - m_values[cons] = self.get_M_value(cons) + for cons in transBlock.private_data().bigm_src: + m_values[cons] = self.get_M_value(cons) return m_values def get_largest_M_value(self, model): From c9232e9439507919a714cfd3d7dd31b724ada14c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:45:57 -0700 Subject: [PATCH 0520/1178] black --- pyomo/gdp/plugins/bigm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index bcf9606877b..3f450dbbd4f 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -61,6 +61,7 @@ class _BigMData(AutoSlots.Mixin): __slots__ = ('bigm_src',) + def __init__(self): # we will keep a map of constraints (hashable, ha!) to a tuple to # indicate what their M value is and where it came from, of the form: @@ -74,8 +75,10 @@ def __init__(self): # information for both.) self.bigm_src = {} + Block.register_private_data_initializer(_BigMData) + @TransformationFactory.register( 'gdp.bigm', doc="Relax disjunctive model using big-M terms." ) From 1221996f3a940ddc0e0de7240158f4c49551d386 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 13:20:11 -0700 Subject: [PATCH 0521/1178] Making transformed_constraints a DefaultComponentMap --- pyomo/gdp/plugins/bigm_mixin.py | 15 +++---- pyomo/gdp/plugins/binary_multiplication.py | 2 +- .../gdp/plugins/gdp_to_mip_transformation.py | 6 +-- pyomo/gdp/plugins/hull.py | 39 +++++++------------ pyomo/gdp/plugins/multiple_bigm.py | 16 +++----- pyomo/gdp/tests/test_bigm.py | 36 ++++++----------- pyomo/gdp/tests/test_hull.py | 17 +++----- pyomo/gdp/util.py | 14 +++---- 8 files changed, 53 insertions(+), 92 deletions(-) diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 59d79331a34..b76c8d43279 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -253,7 +253,8 @@ def _add_constraint_expressions( ) M_expr = M[0] * (1 - indicator_var) newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) - constraint_map.transformed_constraint[c] = [newConstraint[name, i, 'lb']] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'lb']) constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: @@ -263,13 +264,7 @@ def _add_constraint_expressions( ) M_expr = M[1] * (1 - indicator_var) newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) - transformed = constraint_map.transformed_constraint.get(c) - if transformed is not None: - constraint_map.transformed_constraint[c].append( - newConstraint[name, i, 'ub'] - ) - else: - constraint_map.transformed_constraint[c] = [ - newConstraint[name, i, 'ub'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'ub'] + ) constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 6d0955c95a7..bea33580ed6 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -156,7 +156,7 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique - transformed = constraint_map.transformed_constraint[c] = [] + transformed = constraint_map.transformed_constraints[c] lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 94dde433a15..8dcd22b292a 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -12,7 +12,7 @@ from functools import wraps from pyomo.common.autoslots import AutoSlots -from pyomo.common.collections import ComponentMap +from pyomo.common.collections import ComponentMap, DefaultComponentMap from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name @@ -50,11 +50,11 @@ class _GDPTransformationData(AutoSlots.Mixin): - __slots__ = ('src_constraint', 'transformed_constraint') + __slots__ = ('src_constraint', 'transformed_constraints') def __init__(self): self.src_constraint = ComponentMap() - self.transformed_constraint = ComponentMap() + self.transformed_constraints = DefaultComponentMap(list) Block.register_private_data_initializer(_GDPTransformationData, scope='pyomo.gdp') diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index a2b050298e2..1dc6b76e6a6 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -742,7 +742,7 @@ def _transform_constraint( # this variable, so I'm going to return # it. Alternatively we could return an empty list, but I # think I like this better. - constraint_map.transformed_constraint[c] = [v[0]] + constraint_map.transformed_constraints[c].append(v[0]) # Reverse map also (this is strange) constraint_map.src_constraint[v[0]] = c continue @@ -751,9 +751,8 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) - constraint_map.transformed_constraint[c] = [ - newConstraint[name, i, 'eq'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'eq']) constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) @@ -764,9 +763,9 @@ def _transform_constraint( # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the # _ConstraintDatas to each other above) - constraint_map.transformed_constraint[c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, 'eq'] - ] + ) constraint_map.src_constraint[newConstraint[name, 'eq']] = c continue @@ -782,15 +781,15 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'lb'), newConsExpr) - constraint_map.transformed_constraint[c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'lb'] - ] + ) constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c else: newConstraint.add((name, 'lb'), newConsExpr) - constraint_map.transformed_constraint[c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, 'lb'] - ] + ) constraint_map.src_constraint[newConstraint[name, 'lb']] = c if c.upper is not None: @@ -806,23 +805,15 @@ def _transform_constraint( newConstraint.add((name, i, 'ub'), newConsExpr) # map (have to account for fact we might have created list # above - transformed = constraint_map.transformed_constraint.get(c) - if transformed is not None: - transformed.append(newConstraint[name, i, 'ub']) - else: - constraint_map.transformed_constraint[c] = [ - newConstraint[name, i, 'ub'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'ub'] + ) constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c else: newConstraint.add((name, 'ub'), newConsExpr) - transformed = constraint_map.transformed_constraint.get(c) - if transformed is not None: - transformed.append(newConstraint[name, 'ub']) - else: - constraint_map.transformed_constraint[c] = [ - newConstraint[name, 'ub'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, 'ub'] + ) constraint_map.src_constraint[newConstraint[name, 'ub']] = c # deactivate now that we have transformed diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index fccb0514dfb..4dffd4e9f9a 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -378,7 +378,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): continue if not self._config.only_mbigm_bound_constraints: - transformed = [] + transformed = constraint_map.transformed_constraints[c] if c.lower is not None: rhs = sum( Ms[c, disj][0] * disj.indicator_var.get_associated_binary() @@ -398,7 +398,6 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): transformed.append(newConstraint[i, 'ub']) for c_new in transformed: constraint_map.src_constraint[c_new] = [c] - constraint_map.transformed_constraint[c] = transformed else: lower = (None, None, None) upper = (None, None, None) @@ -533,7 +532,7 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): constraint_map.src_constraint[transformed[idx, 'lb']].append(c) disj.transformation_block.private_data( 'pyomo.gdp' - ).transformed_constraint[c] = [transformed[idx, 'lb']] + ).transformed_constraints[c].append(transformed[idx, 'lb']) if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) constraint_map.src_constraint[transformed[idx, 'ub']] = [] @@ -543,14 +542,9 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): disj_constraint_map = disj.transformation_block.private_data( 'pyomo.gdp' ) - if c in disj_constraint_map.transformed_constraint: - disj_constraint_map.transformed_constraint[c].append( - transformed[idx, 'ub'] - ) - else: - disj_constraint_map.transformed_constraint[c] = [ - transformed[idx, 'ub'] - ] + disj_constraint_map.transformed_constraints[c].append( + transformed[idx, 'ub'] + ) return transformed_constraints diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 00efcb46485..ec281218786 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1316,18 +1316,11 @@ def test_do_not_transform_deactivated_constraintDatas(self): bigm.apply_to(m) # the real test: This wasn't transformed - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*b.simpledisj1.c\[1\]", - bigm.get_transformed_constraints, - m.b.simpledisj1.c[1], - ) - self.assertRegex( - log.getvalue(), - r".*Constraint 'b.simpledisj1.c\[1\]' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + ): + bigm.get_transformed_constraints(m.b.simpledisj1.c[1]) # and the rest of the container was transformed cons_list = bigm.get_transformed_constraints(m.b.simpledisj1.c[2]) @@ -2272,18 +2265,13 @@ def check_all_but_evil1_b_anotherblock_constraint_transformed(self, m): self.assertEqual(len(evil1), 2) self.assertIs(evil1[0].parent_block(), disjBlock[1]) self.assertIs(evil1[1].parent_block(), disjBlock[1]) - out = StringIO() - with LoggingIntercept(out, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*.evil\[1\].b.anotherblock.c", - bigm.get_transformed_constraints, - m.evil[1].b.anotherblock.c, - ) - self.assertRegex( - out.getvalue(), - r".*Constraint 'evil\[1\].b.anotherblock.c' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'evil\[1\].b.anotherblock.c' has not been " + r"transformed.", + ): + bigm.get_transformed_constraints(m.evil[1].b.anotherblock.c) + evil1 = bigm.get_transformed_constraints(m.evil[1].bb[1].c) self.assertEqual(len(evil1), 2) self.assertIs(evil1[0].parent_block(), disjBlock[1]) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 98322d4888d..02b3e0152b4 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -897,18 +897,11 @@ def test_do_not_transform_deactivated_constraintDatas(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) # can't ask for simpledisj1.c[1]: it wasn't transformed - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*b.simpledisj1.c\[1\]", - hull.get_transformed_constraints, - m.b.simpledisj1.c[1], - ) - self.assertRegex( - log.getvalue(), - r".*Constraint 'b.simpledisj1.c\[1\]' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + ): + hull.get_transformed_constraints(m.b.simpledisj1.c[1]) # this fixes a[2] to 0, so we should get the disggregated var transformed = hull.get_transformed_constraints(m.b.simpledisj1.c[2]) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index b3e7de8a7cd..e7da03c7f41 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -538,13 +538,13 @@ def get_transformed_constraints(srcConstraint): "from any of its _ComponentDatas.)" ) transBlock = _get_constraint_transBlock(srcConstraint) - try: - return transBlock.private_data('pyomo.gdp').transformed_constraint[ - srcConstraint - ] - except: - logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) - raise + transformed_constraints = transBlock.private_data( + 'pyomo.gdp').transformed_constraints + if srcConstraint in transformed_constraints: + return transformed_constraints[srcConstraint] + else: + raise GDP_Error("Constraint '%s' has not been transformed." % + srcConstraint.name) def _warn_for_active_disjunct(innerdisjunct, outerdisjunct): From 1405731cfa5547d478c3cc9a52e1a60dd61bfa27 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 13:21:50 -0700 Subject: [PATCH 0522/1178] black --- pyomo/gdp/plugins/bigm_mixin.py | 3 ++- pyomo/gdp/plugins/hull.py | 3 ++- pyomo/gdp/tests/test_bigm.py | 6 ++---- pyomo/gdp/tests/test_hull.py | 3 +-- pyomo/gdp/util.py | 8 +++++--- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index b76c8d43279..510b36b5102 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -254,7 +254,8 @@ def _add_constraint_expressions( M_expr = M[0] * (1 - indicator_var) newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) constraint_map.transformed_constraints[c].append( - newConstraint[name, i, 'lb']) + newConstraint[name, i, 'lb'] + ) constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 1dc6b76e6a6..5b9d2ad08a9 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -752,7 +752,8 @@ def _transform_constraint( newConstraint.add((name, i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) constraint_map.transformed_constraints[c].append( - newConstraint[name, i, 'eq']) + newConstraint[name, i, 'eq'] + ) constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index ec281218786..2383d4587f5 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1317,8 +1317,7 @@ def test_do_not_transform_deactivated_constraintDatas(self): # the real test: This wasn't transformed with self.assertRaisesRegex( - GDP_Error, - r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + GDP_Error, r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." ): bigm.get_transformed_constraints(m.b.simpledisj1.c[1]) @@ -2267,8 +2266,7 @@ def check_all_but_evil1_b_anotherblock_constraint_transformed(self, m): self.assertIs(evil1[1].parent_block(), disjBlock[1]) with self.assertRaisesRegex( GDP_Error, - r"Constraint 'evil\[1\].b.anotherblock.c' has not been " - r"transformed.", + r"Constraint 'evil\[1\].b.anotherblock.c' has not been transformed.", ): bigm.get_transformed_constraints(m.evil[1].b.anotherblock.c) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 02b3e0152b4..55edf244731 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -898,8 +898,7 @@ def test_do_not_transform_deactivated_constraintDatas(self): hull.apply_to(m) # can't ask for simpledisj1.c[1]: it wasn't transformed with self.assertRaisesRegex( - GDP_Error, - r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + GDP_Error, r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." ): hull.get_transformed_constraints(m.b.simpledisj1.c[1]) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index e7da03c7f41..fe11975954d 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -539,12 +539,14 @@ def get_transformed_constraints(srcConstraint): ) transBlock = _get_constraint_transBlock(srcConstraint) transformed_constraints = transBlock.private_data( - 'pyomo.gdp').transformed_constraints + 'pyomo.gdp' + ).transformed_constraints if srcConstraint in transformed_constraints: return transformed_constraints[srcConstraint] else: - raise GDP_Error("Constraint '%s' has not been transformed." % - srcConstraint.name) + raise GDP_Error( + "Constraint '%s' has not been transformed." % srcConstraint.name + ) def _warn_for_active_disjunct(innerdisjunct, outerdisjunct): From 58996fa1e4da2df12a5e0d6290ff95ed178d2603 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 13:12:58 -0700 Subject: [PATCH 0523/1178] fix division by zero error in linear presolve --- pyomo/repn/plugins/nl_writer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3fd97ac06d0..a256cd1b900 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1829,15 +1829,6 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): if expr_info.linear[x] == 0: nnz -= 1 coef = expr_info.linear.pop(x) - if not nnz: - if abs(expr_info.const) > TOL: - # constraint is trivially infeasible - raise InfeasibleConstraintException( - "model contains a trivially infeasible constraint " - f"{expr_info.const} == {coef}*{var_map[x]}" - ) - # constraint is trivially feasible - eliminated_cons.add(con_id) elif a: expr_info.linear[x] = c * a # replacing _id with x... NNZ is not changing, @@ -1847,6 +1838,15 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): continue _old = lcon_by_linear_nnz[old_nnz] if con_id in _old: + if not nnz: + if abs(expr_info.const) > TOL: + # constraint is trivially infeasible + raise InfeasibleConstraintException( + "model contains a trivially infeasible constraint " + f"{expr_info.const} == {coef}*{var_map[x]}" + ) + # constraint is trivially feasible + eliminated_cons.add(con_id) lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id) # If variables were replaced by the variable that # we are currently eliminating, then we need to update From 9bf36c7f81367449ba0441a0840487bb122320bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 25 Feb 2024 13:26:51 -0700 Subject: [PATCH 0524/1178] Adding tests to NLv2 motivated by 58996fa --- pyomo/repn/tests/ampl/test_nlv2.py | 125 +++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 8b95fc03bdb..215715dba10 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1683,6 +1683,131 @@ def test_presolve_named_expressions(self): G0 2 #obj 0 0 1 0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_zero_coef(self): + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.obj = Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = Constraint(expr=m.z == -m.y) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual(nlinfo.eliminated_vars[0], (m.x, 1.5)) + self.assertIs(nlinfo.eliminated_vars[1][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[1][1], LinearExpression([-1.0 * m.z]) + ) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n1.5 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #0 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 +""", + OUT.getvalue(), + ) + ) + + m.c3 = Constraint(expr=m.x == 2) + OUT = io.StringIO() + with LoggingIntercept() as LOG: + with self.assertRaisesRegex( + nl_writer.InfeasibleConstraintException, + r"model contains a trivially infeasible constraint 0.5 == 0.0\*y", + ): + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + m.c1.set_value(m.x >= m.y + m.z + 1.5) + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[0][1], LinearExpression([-1.0 * m.z]) + ) + self.assertEqual(nlinfo.eliminated_vars[1], (m.x, 2)) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #c1 +n0 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n2 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #1 ranges (rhs's) +1 0.5 #c1 +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 """, OUT.getvalue(), ) From fca1035351fec7f189c07557705f340a00d71dae Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 25 Feb 2024 16:01:49 -0700 Subject: [PATCH 0525/1178] allow cyipopt to solve problems without objectives --- .../algorithms/solvers/cyipopt_solver.py | 23 +++++++++++++++---- .../solvers/tests/test_cyipopt_solver.py | 10 ++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index 9d24c0dd562..cdea542295b 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -24,6 +24,8 @@ from pyomo.common.deprecation import relocated_module_attribute from pyomo.common.dependencies import attempt_import, numpy as np, numpy_available from pyomo.common.tee import redirect_fd, TeeStream +from pyomo.common.modeling import unique_component_name +from pyomo.core.base.objective import Objective # Because pynumero.interfaces requires numpy, we will leverage deferred # imports here so that the solver can be registered even when numpy is @@ -332,11 +334,22 @@ def solve(self, model, **kwds): grey_box_blocks = list( model.component_data_objects(egb.ExternalGreyBoxBlock, active=True) ) - if grey_box_blocks: - # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) - nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) - else: - nlp = pyomo_nlp.PyomoNLP(model) + # if there is no objective, add one temporarily so we can construct an NLP + objectives = list(model.component_data_objects(Objective, active=True)) + if not objectives: + objname = unique_component_name(model, "_obj") + objective = model.add_component(objname, Objective(expr=0.0)) + try: + if grey_box_blocks: + # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) + nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) + else: + nlp = pyomo_nlp.PyomoNLP(model) + finally: + # We only need the objective to construct the NLP, so we delete + # it from the model ASAP + if not objectives: + model.del_component(objective) problem = cyipopt_interface.CyIpoptNLP( nlp, diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py index e9da31097a0..0af5a772c98 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -316,3 +316,13 @@ def test_hs071_evalerror_old_cyipopt(self): msg = "Error in AMPL evaluation" with self.assertRaisesRegex(PyNumeroEvaluationError, msg): res = solver.solve(m, tee=True) + + def test_solve_without_objective(self): + m = create_model1() + m.o.deactivate() + m.x[2].fix(0.0) + m.x[3].fix(4.0) + solver = pyo.SolverFactory("cyipopt") + res = solver.solve(m, tee=True) + pyo.assert_optimal_termination(res) + self.assertAlmostEqual(m.x[1].value, 9.0) From 242ba7f424eb351ad250c48963627b30e43d2a3c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 25 Feb 2024 17:15:39 -0700 Subject: [PATCH 0526/1178] Updating the TPL package list due to contrib.solver --- pyomo/environ/tests/test_environ.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/tests/test_environ.py b/pyomo/environ/tests/test_environ.py index 9c89fd135d5..6121a310024 100644 --- a/pyomo/environ/tests/test_environ.py +++ b/pyomo/environ/tests/test_environ.py @@ -140,6 +140,7 @@ def test_tpl_import_time(self): 'cPickle', 'csv', 'ctypes', # mandatory import in core/base/external.py; TODO: fix this + 'datetime', # imported by contrib.solver 'decimal', 'gc', # Imported on MacOS, Windows; Linux in 3.10 'glob', From 2edbf24a6cde120889cc7a1d32bda814f3d9379a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 25 Feb 2024 17:21:30 -0700 Subject: [PATCH 0527/1178] Apply black --- pyomo/environ/tests/test_environ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/environ/tests/test_environ.py b/pyomo/environ/tests/test_environ.py index 6121a310024..9811b412af7 100644 --- a/pyomo/environ/tests/test_environ.py +++ b/pyomo/environ/tests/test_environ.py @@ -140,7 +140,7 @@ def test_tpl_import_time(self): 'cPickle', 'csv', 'ctypes', # mandatory import in core/base/external.py; TODO: fix this - 'datetime', # imported by contrib.solver + 'datetime', # imported by contrib.solver 'decimal', 'gc', # Imported on MacOS, Windows; Linux in 3.10 'glob', From 7d88fe4aee8a97e0e199e95c3f7e29064bccd3fa Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Mon, 26 Feb 2024 17:05:40 +0100 Subject: [PATCH 0528/1178] Added MAiNGO appsi-interface --- pyomo/contrib/appsi/solvers/__init__.py | 1 + pyomo/contrib/appsi/solvers/maingo.py | 653 ++++++++++++++++++++++++ 2 files changed, 654 insertions(+) create mode 100644 pyomo/contrib/appsi/solvers/maingo.py diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c03523a69d4..c9e0a2a003d 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,3 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults +from .maingo import MAiNGO \ No newline at end of file diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py new file mode 100644 index 00000000000..dcb8040eabe --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -0,0 +1,653 @@ +from collections import namedtuple +import logging +import math +import sys +from typing import Optional, List, Dict + +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available +from pyomo.common.collections import ComponentMap +from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.log import LogStream +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.expression import ScalarExpression +from pyomo.core.base.param import _ParamData +from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.var import Var, _GeneralVarData +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.staleflag import StaleFlagManager +from pyomo.repn.util import valid_expr_ctypes_minlp + +_plusMinusOne = {-1, 1} + +MaingoVar = namedtuple("MaingoVar", "type name lb ub init") + +logger = logging.getLogger(__name__) + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + MAiNGO._available = MAiNGO.Availability.NotFound + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + + +class MAiNGOConfig(MIPSolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MAiNGOConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare("logfile", ConfigValue(domain=str)) + self.declare("solver_output_logger", ConfigValue()) + self.declare("log_level", ConfigValue(domain=NonNegativeInt)) + + self.logfile = "" + self.solver_output_logger = logger + self.log_level = logging.INFO + + +class MAiNGOSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None): + self._assert_solution_still_valid() + self._solver.load_vars(vars_to_load=vars_to_load) + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + +class MAiNGOResults(Results): + def __init__(self, solver): + super(MAiNGOResults, self).__init__() + self.wallclock_time = None + self.cpu_time = None + self.solution_loader = MAiNGOSolutionLoader(solver=solver) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + + def build_maingo_objective(self, obj, visitor): + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + maingo_obj *= -1 + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + return [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + + def get_initial_point(self): + return [var.init if not var.init is None else var.lb for var in self._var_list] + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result + + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.inv(maingopy.sinh(x)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.inv(maingopy.cosh(x)) + + @classmethod + def maingo_atanh(cls, x): + return maingopy.inv(maingopy.tanh(x)) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + const, var = node.args + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _linear_to_maingo(self, node): + values = [ + self._monomial_to_maingo(arg) + if ( + arg.__class__ is EXPR.MonomialTermExpression + and not arg.arg(1).is_fixed() + ) + else value(arg) + for arg in node.args + ] + return sum(values) + + +class MAiNGO(PersistentBase, PersistentSolver): + """ + Interface to MAiNGO + """ + + _available = None + + def __init__(self, only_child_vars=False): + super(MAiNGO, self).__init__(only_child_vars=only_child_vars) + self._config = MAiNGOConfig() + self._solver_options = dict() + self._solver_model = None + self._mymaingo = None + self._symbol_map = SymbolMap() + self._labeler = None + self._maingo_vars = [] + self._objective = None + self._cons = [] + self._pyomo_var_to_solver_var_id_map = dict() + self._last_results_object: Optional[MAiNGOResults] = None + + def available(self): + if not maingopy_available: + return self.Availability.NotFound + self._available = True + return self._available + + def version(self): + pass + + @property + def config(self) -> MAiNGOConfig: + return self._config + + @config.setter + def config(self, val: MAiNGOConfig): + self._config = val + + @property + def maingo_options(self): + """ + A dictionary mapping solver options to values for those options. These + are solver specific. + + Returns + ------- + dict + A dictionary mapping solver options to values for those options + """ + return self._solver_options + + @maingo_options.setter + def maingo_options(self, val: Dict): + self._solver_options = val + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self, timer: HierarchicalTimer): + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.stream_solver: + ostreams.append(sys.stdout) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + config = self.config + options = self.maingo_options + + self._mymaingo = maingopy.MAiNGO(self._solver_model) + + self._mymaingo.set_option("loggingDestination", 2) + self._mymaingo.set_log_file_name(config.logfile) + + if config.time_limit is not None: + self._mymaingo.set_option("maxTime", config.time_limit) + if config.mip_gap is not None: + self._mymaingo.set_option("epsilonA", config.mip_gap) + for key, option in options.items(): + self._mymaingo.set_option(key, option) + + timer.start("MAiNGO solve") + self._mymaingo.solve() + timer.stop("MAiNGO solve") + + return self._postsolve(timer) + + def solve(self, model, timer: HierarchicalTimer = None): + StaleFlagManager.mark_all_as_stale() + + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if timer is None: + timer = HierarchicalTimer() + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + res = self._solve(timer) + self._last_results_object = res + if self.config.report_timing: + logger.info("\n" + str(timer)) + return res + + def _process_domain_and_bounds(self, var): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + if step == 0: + vtype = maingopy.VT_CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = maingopy.VT_BINARY + else: + vtype = maingopy.VT_INTEGER + else: + raise ValueError( + f"Unrecognized domain step: {step} (should be either 0 or 1)" + ) + if _fixed: + lb = _value + ub = _value + else: + if _lb is not None: + lb = max(value(_lb), lb) + if _ub is not None: + ub = min(value(_ub), ub) + + return lb, ub, vtype + + def _add_variables(self, variables: List[_GeneralVarData]): + for ndx, var in enumerate(variables): + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars.append( + MaingoVar(name=varname, type=vtype, lb=lb, ub=ub, init=var.value) + ) + self._pyomo_var_to_solver_var_id_map[id(var)] = len(self._maingo_vars) - 1 + + def _add_params(self, params: List[_ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_options = self.maingo_options + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.config = saved_config + self.maingo_options = saved_options + self.update_config = saved_update_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f"Solver {c.__module__}.{c.__qualname__} is not available " + f"({self.available()})." + ) + self._reinit() + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler("x") + + self.add_block(model) + self._solver_model = SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + ) + + def _add_constraints(self, cons: List[_GeneralConstraintData]): + self._cons = cons + + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def _remove_params(self, params: List[_ParamData]): + pass + + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_params(self): + pass + + def _set_objective(self, obj): + if obj is None: + raise NotImplementedError( + "MAiNGO needs a objective. Please set a dummy objective." + ) + else: + if not obj.sense in {minimize, maximize}: + raise ValueError( + "Objective sense is not recognized: {0}".format(obj.sense) + ) + self._objective = obj + + def _postsolve(self, timer: HierarchicalTimer): + config = self.config + + mprob = self._mymaingo + status = mprob.get_status() + results = MAiNGOResults(solver=self) + results.wallclock_time = mprob.get_wallclock_solution_time() + results.cpu_time = mprob.get_cpu_solution_time() + + if status == maingopy.GLOBALLY_OPTIMAL: + results.termination_condition = TerminationCondition.optimal + elif status == maingopy.INFEASIBLE: + results.termination_condition = TerminationCondition.infeasible + else: + results.termination_condition = TerminationCondition.unknown + + results.best_feasible_objective = None + results.best_objective_bound = None + if self._objective is not None: + try: + if self._objective.sense == maximize: + results.best_feasible_objective = -mprob.get_objective_value() + else: + results.best_feasible_objective = mprob.get_objective_value() + except: + results.best_feasible_objective = None + try: + if self._objective.sense == maximize: + results.best_objective_bound = -mprob.get_final_LBD() + else: + results.best_objective_bound = mprob.get_final_LBD() + except: + if self._objective.sense == maximize: + results.best_objective_bound = math.inf + else: + results.best_objective_bound = -math.inf + + if results.best_feasible_objective is not None and not math.isfinite( + results.best_feasible_objective + ): + results.best_feasible_objective = None + + timer.start("load solution") + if config.load_solution: + if not results.best_feasible_objective is None: + if results.termination_condition != TerminationCondition.optimal: + logger.warning( + "Loading a feasible but suboptimal solution. " + "Please set load_solution=False and check " + "results.termination_condition and " + "results.found_feasible_solution() before loading a solution." + ) + self.load_vars() + else: + raise RuntimeError( + "A feasible solution was not found, so no solution can be loaded." + "Please set opt.config.load_solution=False and check " + "results.termination_condition and " + "results.best_feasible_objective before loading a solution." + ) + timer.stop("load solution") + + return results + + def load_vars(self, vars_to_load=None): + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load=None): + if not self._mymaingo.get_status() in { + maingopy.GLOBALLY_OPTIMAL, + maingopy.FEASIBLE_POINT, + }: + raise RuntimeError( + "Solver does not currently have a valid solution." + "Please check the termination condition." + ) + + var_id_map = self._pyomo_var_to_solver_var_id_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = var_id_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + maingo_var_ids_to_load = [ + var_id_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + + solution_point = self._mymaingo.get_solution_point() + vals = [solution_point[var_id] for var_id in maingo_var_ids_to_load] + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def get_reduced_costs(self, vars_to_load=None): + raise ValueError("MAiNGO does not support returning Reduced Costs") + + def get_duals(self, cons_to_load=None): + raise ValueError("MAiNGO does not support returning Duals") From 0ea6a293df516acb7a02ba4ec56a51b51009cbc6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:25:06 -0700 Subject: [PATCH 0529/1178] Improve registration of new native types encountered by ExternalFunction --- pyomo/common/numeric_types.py | 108 +++++++++++++++++++++++++++------- pyomo/core/base/external.py | 14 +++-- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index ba104203667..f24b007b096 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -194,6 +194,53 @@ def RegisterLogicalType(new_type: type): nonpyomo_leaf_types.add(new_type) +def check_if_native_type(obj): + if isinstance(obj, (str, bytes)): + native_types.add(obj.__class__) + return True + if check_if_logical_type(obj): + return True + if check_if_numeric_type(obj): + return True + return False + + +def check_if_logical_type(obj): + """Test if the argument behaves like a logical type. + + We check for "numeric types" by checking if we can add zero to it + without changing the object's type, and that the object compares to + 0 in a meaningful way. If that works, then we register the type in + :py:attr:`native_numeric_types`. + + """ + obj_class = obj.__class__ + # Do not re-evaluate known native types + if obj_class in native_types: + return obj_class in native_logical_types + + if 'numpy' in obj_class.__module__: + # trigger the resolution of numpy_available and check if this + # type was automatically registered + bool(numpy_available) + if obj_class in native_types: + return obj_class in native_logical_types + + try: + if all(( + obj_class(1) == obj_class(2), + obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(False) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(True) == obj_class(False), + )): + RegisterLogicalType(obj_class) + return True + except: + pass + return False + + def check_if_numeric_type(obj): """Test if the argument behaves like a numeric type. @@ -218,36 +265,53 @@ def check_if_numeric_type(obj): try: obj_plus_0 = obj + 0 obj_p0_class = obj_plus_0.__class__ - # ensure that the object is comparable to 0 in a meaningful way - # (among other things, this prevents numpy.ndarray objects from - # being added to native_numeric_types) + # Native numeric types *must* be hashable + hash(obj) + except: + return False + if obj_p0_class is not obj_class and obj_p0_class not in native_numeric_types: + return False + # + # Check if the numeric type behaves like a complex type + # + try: + if 1.41 < abs(obj_class(1j+1)) < 1.42: + RegisterComplexType(obj_class) + return False + except: + pass + # + # ensure that the object is comparable to 0 in a meaningful way + # (among other things, this prevents numpy.ndarray objects from + # being added to native_numeric_types) + try: if not ((obj < 0) ^ (obj >= 0)): return False - # Native types *must* be hashable - hash(obj) except: return False - if obj_p0_class is obj_class or obj_p0_class in native_numeric_types: - # - # If we get here, this is a reasonably well-behaving - # numeric type: add it to the native numeric types - # so that future lookups will be faster. - # - RegisterNumericType(obj_class) - # - # Generate a warning, since Pyomo's management of third-party - # numeric types is more robust when registering explicitly. - # - logger.warning( - f"""Dynamically registering the following numeric type: + # + # If we get here, this is a reasonably well-behaving + # numeric type: add it to the native numeric types + # so that future lookups will be faster. + # + RegisterNumericType(obj_class) + try: + if obj_class(0.4) == obj_class(0): + RegisterIntegerType(obj_class) + except: + pass + # + # Generate a warning, since Pyomo's management of third-party + # numeric types is more robust when registering explicitly. + # + logger.warning( + f"""Dynamically registering the following numeric type: {obj_class.__module__}.{obj_class.__name__} Dynamic registration is supported for convenience, but there are known limitations to this approach. We recommend explicitly registering numeric types using RegisterNumericType() or RegisterIntegerType().""" - ) - return True - else: - return False + ) + return True def value(obj, exception=True): diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index 3c0038d745d..cae62d31941 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -31,13 +31,16 @@ from pyomo.common.autoslots import AutoSlots from pyomo.common.fileutils import find_library -from pyomo.core.expr.numvalue import ( +from pyomo.common.numeric_types import ( + check_if_native_type, native_types, native_numeric_types, pyomo_constant_types, + value, +) +from pyomo.core.expr.numvalue import ( NonNumericValue, NumericConstant, - value, ) import pyomo.core.expr as EXPR from pyomo.core.base.component import Component @@ -197,14 +200,15 @@ def __call__(self, *args): pv = False for i, arg in enumerate(args_): try: - # Q: Is there a better way to test if a value is an object - # not in native_types and not a standard expression type? if arg.__class__ in native_types: continue if arg.is_potentially_variable(): pv = True + continue except AttributeError: - args_[i] = NonNumericValue(arg) + if check_if_native_type(arg): + continue + args_[i] = NonNumericValue(arg) # if pv: return EXPR.ExternalFunctionExpression(args_, self) From bf15d23e3c46c1735d9ff4b3050e85947a9760a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:26:06 -0700 Subject: [PATCH 0530/1178] Deprecate the pyomo_constant_types set --- pyomo/common/numeric_types.py | 21 ++++++++++----------- pyomo/core/base/external.py | 4 ++-- pyomo/core/base/units_container.py | 5 ++--- pyomo/core/expr/numvalue.py | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index f24b007b096..0cfdd347484 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -50,7 +50,6 @@ native_integer_types = {int} native_logical_types = {bool} native_complex_types = {complex} -pyomo_constant_types = set() # includes NumericConstant _native_boolean_types = {int, bool, str, bytes} relocated_module_attribute( @@ -62,6 +61,16 @@ "be treated as if they were bool (as was the case for the other " "native_*_types sets). Users likely should use native_logical_types.", ) +_pyomo_constant_types = set() # includes NumericConstant, _PythonCallbackFunctionID +relocated_module_attribute( + 'pyomo_constant_types', + 'pyomo.common.numeric_types._pyomo_constant_types', + version='6.7.2.dev0', + msg="The pyomo_constant_types set will be removed in the future: the set " + "contained only NumericConstant and _PythonCallbackFunctionID, and provided " + "no meaningful value to clients or walkers. Users should likely handle " + "these types in the same manner as immutable Params.", +) #: Python set used to identify numeric constants and related native @@ -338,16 +347,6 @@ def value(obj, exception=True): """ if obj.__class__ in native_types: return obj - if obj.__class__ in pyomo_constant_types: - # - # I'm commenting this out for now, but I think we should never expect - # to see a numeric constant with value None. - # - # if exception and obj.value is None: - # raise ValueError( - # "No value for uninitialized NumericConstant object %s" - # % (obj.name,)) - return obj.value # # Test if we have a duck typed Pyomo expression # diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index cae62d31941..92e7286f3d4 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -35,8 +35,8 @@ check_if_native_type, native_types, native_numeric_types, - pyomo_constant_types, value, + _pyomo_constant_types, ) from pyomo.core.expr.numvalue import ( NonNumericValue, @@ -495,7 +495,7 @@ def is_constant(self): return False -pyomo_constant_types.add(_PythonCallbackFunctionID) +_pyomo_constant_types.add(_PythonCallbackFunctionID) class PythonCallbackFunction(ExternalFunction): diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index 1bf25ffdead..af8c77b25aa 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -119,7 +119,6 @@ value, native_types, native_numeric_types, - pyomo_constant_types, ) from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr.visitor import ExpressionValueVisitor @@ -902,7 +901,7 @@ def initializeWalker(self, expr): def beforeChild(self, node, child, child_idx): ctype = child.__class__ - if ctype in native_types or ctype in pyomo_constant_types: + if ctype in native_types: return False, self._pint_dimensionless if child.is_expression_type(): @@ -917,7 +916,7 @@ def beforeChild(self, node, child, child_idx): pint_unit = self._pyomo_units_container._get_pint_units(pyomo_unit) return False, pint_unit - return True, None + return False, self._pint_dimensionless def exitNode(self, node, data): """Visitor callback when moving up the expression tree. diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 3a4359af2f9..95914248bc7 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -28,7 +28,7 @@ native_numeric_types, native_integer_types, native_logical_types, - pyomo_constant_types, + _pyomo_constant_types, check_if_numeric_type, value, ) @@ -410,7 +410,7 @@ def pprint(self, ostream=None, verbose=False): ostream.write(str(self)) -pyomo_constant_types.add(NumericConstant) +_pyomo_constant_types.add(NumericConstant) # We use as_numeric() so that the constant is also in the cache ZeroConstant = as_numeric(0) From 5373c333746e219c0fdc3b202d73bac55f449da3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:27:31 -0700 Subject: [PATCH 0531/1178] Improve efficiency of value() --- pyomo/common/numeric_types.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 0cfdd347484..a234bb4df05 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -350,9 +350,7 @@ def value(obj, exception=True): # # Test if we have a duck typed Pyomo expression # - try: - obj.is_numeric_type() - except AttributeError: + if not hasattr(obj, 'is_numeric_type'): # # TODO: Historically we checked for new *numeric* types and # raised exceptions for anything else. That is inconsistent @@ -367,7 +365,7 @@ def value(obj, exception=True): return None raise TypeError( "Cannot evaluate object with unknown type: %s" % obj.__class__.__name__ - ) from None + ) # # Evaluate the expression object # From 6830e2787aa49fd48b29a0cb7316a4f7f2aa1841 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:28:00 -0700 Subject: [PATCH 0532/1178] Add NonNumericValue to teh PyomoObject hierarchy, define __call__ --- pyomo/core/expr/numvalue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 95914248bc7..64ef0a6cca2 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -85,7 +85,7 @@ ##------------------------------------------------------------------------ -class NonNumericValue(object): +class NonNumericValue(PyomoObject): """An object that contains a non-numeric value Constructor Arguments: @@ -100,6 +100,8 @@ def __init__(self, value): def __str__(self): return str(self.value) + def __call__(self, exception=None): + return self.value nonpyomo_leaf_types.add(NonNumericValue) From 10281708b1efef82751d1f53b8a6ae645321a0ac Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:28:48 -0700 Subject: [PATCH 0533/1178] NFC: apply black --- pyomo/common/numeric_types.py | 18 ++++++++++-------- pyomo/core/base/external.py | 5 +---- pyomo/core/expr/numvalue.py | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index a234bb4df05..412a1bbeade 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -236,13 +236,15 @@ def check_if_logical_type(obj): return obj_class in native_logical_types try: - if all(( - obj_class(1) == obj_class(2), - obj_class(False) != obj_class(True), - obj_class(False) ^ obj_class(True) == obj_class(True), - obj_class(False) | obj_class(True) == obj_class(True), - obj_class(False) & obj_class(True) == obj_class(False), - )): + if all( + ( + obj_class(1) == obj_class(2), + obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(False) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(True) == obj_class(False), + ) + ): RegisterLogicalType(obj_class) return True except: @@ -284,7 +286,7 @@ def check_if_numeric_type(obj): # Check if the numeric type behaves like a complex type # try: - if 1.41 < abs(obj_class(1j+1)) < 1.42: + if 1.41 < abs(obj_class(1j + 1)) < 1.42: RegisterComplexType(obj_class) return False except: diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index 92e7286f3d4..0fda004b664 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -38,10 +38,7 @@ value, _pyomo_constant_types, ) -from pyomo.core.expr.numvalue import ( - NonNumericValue, - NumericConstant, -) +from pyomo.core.expr.numvalue import NonNumericValue, NumericConstant import pyomo.core.expr as EXPR from pyomo.core.base.component import Component from pyomo.core.base.units_container import units diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 64ef0a6cca2..b656eea1bcd 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -103,6 +103,7 @@ def __str__(self): def __call__(self, exception=None): return self.value + nonpyomo_leaf_types.add(NonNumericValue) From 28cf0695f7fc9140f20dcb846c9deff8897cffe1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 12:32:01 -0700 Subject: [PATCH 0534/1178] timing calls and some performance improvements --- .../contrib/incidence_analysis/scc_solver.py | 97 ++++++++++++++++--- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 835e07c7c02..f6697b11567 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -13,18 +13,27 @@ from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint -from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks +from pyomo.util.subsystems import ( + TemporarySubsystemManager, + generate_subsystem_blocks, + create_subsystem_block, +) from pyomo.contrib.incidence_analysis.interface import ( IncidenceGraphInterface, _generate_variables_in_constraints, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod _log = logging.getLogger(__name__) +from pyomo.common.timing import HierarchicalTimer def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False + constraints, + variables=None, + include_fixed=False, + timer=None, ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization @@ -53,27 +62,52 @@ def generate_strongly_connected_components( "input variables" for that block. """ - if variables is None: - variables = list( - _generate_variables_in_constraints(constraints, include_fixed=include_fixed) - ) + if timer is None: + timer = HierarchicalTimer() + + if isinstance(constraints, IncidenceGraphInterface): + igraph = constraints + variables = igraph.variables + constraints = igraph.constraints + else: + if variables is None: + timer.start("generate-variables") + variables = list( + _generate_variables_in_constraints(constraints, include_fixed=include_fixed) + ) + timer.stop("generate-variables") + timer.start("igraph") + igraph = IncidenceGraphInterface() + timer.stop("igraph") assert len(variables) == len(constraints) - igraph = IncidenceGraphInterface() + + timer.start("block-triang") var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) + timer.stop("block-triang") subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] + timer.start("generate-block") for block, inputs in generate_subsystem_blocks( subsets, include_fixed=include_fixed ): + timer.stop("generate-block") # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) + # Note that this code, after the last yield, I believe is only called + # at time of GC. + timer.start("generate-block") + timer.stop("generate-block") def solve_strongly_connected_components( - block, solver=None, solve_kwds=None, calc_var_kwds=None + block, + solver=None, + solve_kwds=None, + calc_var_kwds=None, + timer=None, ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -110,24 +144,59 @@ def solve_strongly_connected_components( solve_kwds = {} if calc_var_kwds is None: calc_var_kwds = {} + if timer is None: + timer = HierarchicalTimer() + timer.start("igraph") igraph = IncidenceGraphInterface( - block, active=True, include_fixed=False, include_inequality=False + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, ) + timer.stop("igraph") + # Use IncidenceGraphInterface to get the constraints and variables constraints = igraph.constraints variables = igraph.variables + timer.start("block-triang") + var_blocks, con_blocks = igraph.block_triangularize() + timer.stop("block-triang") + timer.start("subsystem-blocks") + subsystem_blocks = [ + create_subsystem_block(conbl, varbl, timer=timer) if len(varbl) > 1 else None + for varbl, conbl in zip(var_blocks, con_blocks) + ] + timer.stop("subsystem-blocks") + res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) - for scc, inputs in generate_strongly_connected_components(constraints, variables): + + #timer.start("generate-scc") + #for scc, inputs in generate_strongly_connected_components(igraph, timer=timer): + # timer.stop("generate-scc") + for i, scc in enumerate(subsystem_blocks): + if scc is None: + # Since a block is not necessary for 1x1 solve, we use the convention + # that None indicates a 1x1 SCC. + inputs = [] + var = var_blocks[i][0] + con = con_blocks[i][0] + else: + inputs = list(scc.input_vars.values()) + with TemporarySubsystemManager(to_fix=inputs): - N = len(scc.vars) + N = len(var_blocks[i]) if N == 1: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") + timer.start("calc-var") results = calculate_variable_from_constraint( - scc.vars[0], scc.cons[0], **calc_var_kwds + #scc.vars[0], scc.cons[0], **calc_var_kwds + var, con, **calc_var_kwds ) + timer.stop("calc-var") res_list.append(results) else: if solver is None: @@ -141,6 +210,10 @@ def solve_strongly_connected_components( ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") + timer.start("solve") results = solver.solve(scc, **solve_kwds) + timer.stop("solve") res_list.append(results) + # timer.start("generate-scc") + #timer.stop("generate-scc") return res_list From 2b47212ab8c5db29033e4ffd8b7cd2edb86272cb Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 12:32:23 -0700 Subject: [PATCH 0535/1178] dont repeat work for the same named expression in ExternalFunctionVisitor --- pyomo/util/subsystems.py | 78 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 70a0af1b2a7..43246da37a4 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -20,15 +20,32 @@ from pyomo.core.base.external import ExternalFunction from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.core.expr.numeric_expr import ExternalFunctionExpression -from pyomo.core.expr.numvalue import native_types +from pyomo.core.expr.numvalue import native_types, NumericValue class _ExternalFunctionVisitor(StreamBasedExpressionVisitor): + + def __init__(self, descend_into_named_expressions=True): + super().__init__() + self._descend_into_named_expressions = descend_into_named_expressions + self.named_expressions = [] + def initializeWalker(self, expr): self._functions = [] self._seen = set() return True, None + def beforeChild(self, parent, child, index): + if ( + not self._descend_into_named_expressions + and isinstance(child, NumericValue) + and child.is_named_expression_type() + ): + self.named_expressions.append(child) + return False, None + else: + return True, None + def exitNode(self, node, data): if type(node) is ExternalFunctionExpression: if id(node) not in self._seen: @@ -50,14 +67,47 @@ def acceptChildResult(self, node, data, child_result, child_idx): return child_result.is_expression_type(), None -def identify_external_functions(expr): - yield from _ExternalFunctionVisitor().walk_expression(expr) +def identify_external_functions( + expr, + descend_into_named_expressions=True, + named_expressions=None, +): + visitor = _ExternalFunctionVisitor( + descend_into_named_expressions=descend_into_named_expressions + ) + efs = list(visitor.walk_expression(expr)) + if not descend_into_named_expressions and named_expressions is not None: + named_expressions.extend(visitor.named_expressions) + return efs + #yield from _ExternalFunctionVisitor().walk_expression(expr) def add_local_external_functions(block): ef_exprs = [] + named_expressions = [] for comp in block.component_data_objects((Constraint, Expression), active=True): - ef_exprs.extend(identify_external_functions(comp.expr)) + ef_exprs.extend(identify_external_functions( + comp.expr, + descend_into_named_expressions=False, + named_expressions=named_expressions, + )) + named_expr_set = ComponentSet(named_expressions) + named_expressions = list(named_expr_set) + while named_expressions: + expr = named_expressions.pop() + local_named_exprs = [] + ef_exprs.extend(identify_external_functions( + expr, + descend_into_named_expressions=False, + named_expressions=local_named_exprs, + )) + # Only add to the stack named expressions that we have + # not encountered yet. + for local_expr in local_named_exprs: + if local_expr not in named_expr_set: + named_expressions.append(local_expr) + named_expr_set.add(local_expr) + unique_functions = [] fcn_set = set() for expr in ef_exprs: @@ -75,7 +125,13 @@ def add_local_external_functions(block): return fcn_comp_map -def create_subsystem_block(constraints, variables=None, include_fixed=False): +from pyomo.common.timing import HierarchicalTimer +def create_subsystem_block( + constraints, + variables=None, + include_fixed=False, + timer=None, +): """This function creates a block to serve as a subsystem with the specified variables and constraints. To satisfy certain writers, other variables that appear in the constraints must be added to the block as @@ -99,20 +155,32 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): as well as other variables present in the constraints """ + if timer is None: + timer = HierarchicalTimer() if variables is None: variables = [] + timer.start("block") block = Block(concrete=True) + timer.stop("block") + timer.start("reference") block.vars = Reference(variables) block.cons = Reference(constraints) + timer.stop("reference") var_set = ComponentSet(variables) input_vars = [] + timer.start("identify-vars") for con in constraints: for var in identify_variables(con.expr, include_fixed=include_fixed): if var not in var_set: input_vars.append(var) var_set.add(var) + timer.stop("identify-vars") + timer.start("reference") block.input_vars = Reference(input_vars) + timer.stop("reference") + timer.start("external-fcns") add_local_external_functions(block) + timer.stop("external-fcns") return block From 7aaaeed7f6d051a79a06435a4e0c4a0ad3f8a8bc Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 14:19:19 -0700 Subject: [PATCH 0536/1178] scc_solver implementation using SccImplicitFunctionSolver --- .../contrib/incidence_analysis/scc_solver.py | 167 +++++++++++------- 1 file changed, 100 insertions(+), 67 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index f6697b11567..5d1e2e23f2d 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -147,73 +147,106 @@ def solve_strongly_connected_components( if timer is None: timer = HierarchicalTimer() - timer.start("igraph") - igraph = IncidenceGraphInterface( - block, - active=True, - include_fixed=False, - include_inequality=False, - method=IncidenceMethod.ampl_repn, - ) - timer.stop("igraph") - # Use IncidenceGraphInterface to get the constraints and variables - constraints = igraph.constraints - variables = igraph.variables + USE_IMPLICIT = True + if not USE_IMPLICIT: + timer.start("igraph") + igraph = IncidenceGraphInterface( + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, + ) + timer.stop("igraph") + # Use IncidenceGraphInterface to get the constraints and variables + constraints = igraph.constraints + variables = igraph.variables - timer.start("block-triang") - var_blocks, con_blocks = igraph.block_triangularize() - timer.stop("block-triang") - timer.start("subsystem-blocks") - subsystem_blocks = [ - create_subsystem_block(conbl, varbl, timer=timer) if len(varbl) > 1 else None - for varbl, conbl in zip(var_blocks, con_blocks) - ] - timer.stop("subsystem-blocks") - - res_list = [] - log_blocks = _log.isEnabledFor(logging.DEBUG) - - #timer.start("generate-scc") - #for scc, inputs in generate_strongly_connected_components(igraph, timer=timer): - # timer.stop("generate-scc") - for i, scc in enumerate(subsystem_blocks): - if scc is None: - # Since a block is not necessary for 1x1 solve, we use the convention - # that None indicates a 1x1 SCC. - inputs = [] - var = var_blocks[i][0] - con = con_blocks[i][0] - else: - inputs = list(scc.input_vars.values()) - - with TemporarySubsystemManager(to_fix=inputs): - N = len(var_blocks[i]) - if N == 1: - if log_blocks: - _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") - timer.start("calc-var") - results = calculate_variable_from_constraint( - #scc.vars[0], scc.cons[0], **calc_var_kwds - var, con, **calc_var_kwds - ) - timer.stop("calc-var") - res_list.append(results) + timer.start("block-triang") + var_blocks, con_blocks = igraph.block_triangularize() + timer.stop("block-triang") + timer.start("subsystem-blocks") + subsystem_blocks = [ + create_subsystem_block(conbl, varbl, timer=timer) if len(varbl) > 1 else None + for varbl, conbl in zip(var_blocks, con_blocks) + ] + timer.stop("subsystem-blocks") + + res_list = [] + log_blocks = _log.isEnabledFor(logging.DEBUG) + + #timer.start("generate-scc") + #for scc, inputs in generate_strongly_connected_components(igraph, timer=timer): + # timer.stop("generate-scc") + for i, scc in enumerate(subsystem_blocks): + if scc is None: + # Since a block is not necessary for 1x1 solve, we use the convention + # that None indicates a 1x1 SCC. + inputs = [] + var = var_blocks[i][0] + con = con_blocks[i][0] else: - if solver is None: - var_names = [var.name for var in scc.vars.values()][:10] - con_names = [con.name for con in scc.cons.values()][:10] - raise RuntimeError( - "An external solver is required if block has strongly\n" - "connected components of size greater than one (is not" - " a DAG).\nGot an SCC of size %sx%s including" - " components:\n%s\n%s" % (N, N, var_names, con_names) + inputs = list(scc.input_vars.values()) + + with TemporarySubsystemManager(to_fix=inputs): + N = len(var_blocks[i]) + if N == 1: + if log_blocks: + _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") + timer.start("calc-var") + results = calculate_variable_from_constraint( + #scc.vars[0], scc.cons[0], **calc_var_kwds + var, con, **calc_var_kwds ) - if log_blocks: - _log.debug(f"Solving {N}x{N} block.") - timer.start("solve") - results = solver.solve(scc, **solve_kwds) - timer.stop("solve") - res_list.append(results) - # timer.start("generate-scc") - #timer.stop("generate-scc") - return res_list + timer.stop("calc-var") + res_list.append(results) + else: + if solver is None: + var_names = [var.name for var in scc.vars.values()][:10] + con_names = [con.name for con in scc.cons.values()][:10] + raise RuntimeError( + "An external solver is required if block has strongly\n" + "connected components of size greater than one (is not" + " a DAG).\nGot an SCC of size %sx%s including" + " components:\n%s\n%s" % (N, N, var_names, con_names) + ) + if log_blocks: + _log.debug(f"Solving {N}x{N} block.") + timer.start("solve") + results = solver.solve(scc, **solve_kwds) + timer.stop("solve") + res_list.append(results) + # timer.start("generate-scc") + #timer.stop("generate-scc") + return res_list + else: + from pyomo.contrib.pynumero.algorithms.solvers.implicit_functions import ( + SccImplicitFunctionSolver, + ScipySolverWrapper, + ) + timer.start("igraph") + igraph = IncidenceGraphInterface( + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, + ) + timer.stop("igraph") + # Use IncidenceGraphInterface to get the constraints and variables + constraints = igraph.constraints + variables = igraph.variables + + # Construct an implicit function solver with no parameters. (This is just + # a square system solver.) + scc_solver = SccImplicitFunctionSolver( + variables, + constraints, + [], + solver_class=ScipySolverWrapper, + timer=timer, + use_calc_var=False, + ) + # set_parameters triggers the Newton solve. + scc_solver.set_parameters([]) + scc_solver.update_pyomo_model() From aaecb45022782bfcb00e60f29fb8895139c126fc Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 14:19:34 -0700 Subject: [PATCH 0537/1178] additional timing calls in SccImplicitFunctionSolver --- .../algorithms/solvers/implicit_functions.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py index e40580c1161..17b895507f2 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py @@ -32,10 +32,8 @@ NewtonNlpSolver, SecantNewtonNlpSolver, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod from pyomo.contrib.incidence_analysis import IncidenceGraphInterface -from pyomo.contrib.incidence_analysis.scc_solver import ( - generate_strongly_connected_components, -) class NlpSolverBase(object): @@ -133,7 +131,7 @@ class PyomoImplicitFunctionBase(object): """ - def __init__(self, variables, constraints, parameters): + def __init__(self, variables, constraints, parameters, timer=None): """ Arguments --------- @@ -145,11 +143,13 @@ def __init__(self, variables, constraints, parameters): Variables to be treated as inputs to the implicit function """ + if timer is None: + timer = HierarchicalTimer() self._variables = variables self._constraints = constraints self._parameters = parameters self._block_variables = variables + parameters - self._block = create_subsystem_block(constraints, self._block_variables) + self._block = create_subsystem_block(constraints, self._block_variables, timer=timer) def get_variables(self): return self._variables @@ -361,7 +361,9 @@ def __init__( self._solver_options = solver_options self._calc_var_cutoff = 1 if use_calc_var else 0 # NOTE: This super call is only necessary so the get_* methods work - super().__init__(variables, constraints, parameters) + timer.start("super.__init__") + super().__init__(variables, constraints, parameters, timer=timer) + timer.stop("super.__init__") subsystem_list = [ # Switch order in list for compatibility with generate_subsystem_blocks @@ -376,6 +378,7 @@ def __init__( # an equality constraint. constants = [] constant_set = ComponentSet() + timer.start("identify-vars") for con in constraints: for var in identify_variables(con.expr, include_fixed=False): if var not in constant_set and var not in var_param_set: @@ -383,6 +386,7 @@ def __init__( # a var nor param, treat it as a "constant" constant_set.add(var) constants.append(var) + timer.stop("identify-vars") with TemporarySubsystemManager(to_fix=constants): # Temporarily fix "constant" variables so (a) they don't show @@ -390,6 +394,7 @@ def __init__( # they don't appear as additional columns in the NLPs and # ProjectedNLPs. + timer.start("subsystem-blocks") self._subsystem_list = list(generate_subsystem_blocks(subsystem_list)) # These are subsystems that need an external solver, rather than # calculate_variable_from_constraint. _calc_var_cutoff should be either @@ -399,6 +404,7 @@ def __init__( for block, inputs in self._subsystem_list if len(block.vars) > self._calc_var_cutoff ] + timer.stop("subsystem-blocks") # Need a dummy objective to create an NLP for block, inputs in self._solver_subsystem_list: @@ -421,16 +427,20 @@ def __init__( # "Output variable" names are required to construct ProjectedNLPs. # Ideally, we can eventually replace these with variable indices. + timer.start("names") self._solver_subsystem_var_names = [ [var.name for var in block.vars.values()] for block, inputs in self._solver_subsystem_list ] + timer.stop("names") + timer.start("proj-ext-nlp") self._solver_proj_nlps = [ nlp_proj.ProjectedExtendedNLP(nlp, names) for nlp, names in zip( self._solver_subsystem_nlps, self._solver_subsystem_var_names ) ] + timer.stop("proj-ext-nlp") # We will solve the ProjectedNLPs rather than the original NLPs self._timer.start("NlpSolver") @@ -439,6 +449,7 @@ def __init__( for nlp in self._solver_proj_nlps ] self._timer.stop("NlpSolver") + timer.start("input-indices") self._solver_subsystem_input_coords = [ # Coordinates in the NLP, not ProjectedNLP nlp.get_primal_indices(inputs) @@ -446,6 +457,7 @@ def __init__( self._solver_subsystem_nlps, self._solver_subsystem_list ) ] + timer.stop("input-indices") self._n_variables = len(variables) self._n_constraints = len(constraints) @@ -465,6 +477,7 @@ def __init__( ) # Cache the global array-coordinates of each subset of "input" # variables. These are used for updating before each solve. + timer.start("coord-maps") self._local_input_global_coords = [ # If I do not fix "constants" above, I get errors here # that only show up in the CLC models. @@ -481,6 +494,7 @@ def __init__( ) for (block, _) in self._solver_subsystem_list ] + timer.stop("coord-maps") self._timer.stop("__init__") @@ -612,7 +626,7 @@ def update_pyomo_model(self): class SccImplicitFunctionSolver(DecomposedImplicitFunctionBase): def partition_system(self, variables, constraints): self._timer.start("partition") - igraph = IncidenceGraphInterface() + igraph = IncidenceGraphInterface(method=IncidenceMethod.ampl_repn) var_blocks, con_blocks = igraph.block_triangularize(variables, constraints) self._timer.stop("partition") return zip(var_blocks, con_blocks) From e0312525ed13dfd1e437bf65d9892f2975f3bf03 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:22:42 -0700 Subject: [PATCH 0538/1178] Add hooks for automatic registration on external TPL module import --- pyomo/common/dependencies.py | 87 ++++++++++++++++++++++++++++++++++- pyomo/common/numeric_types.py | 8 ---- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9e96fdd5860..9aa6c4c4f7a 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -9,13 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import Mapping import inspect import importlib import logging import sys import warnings +from collections.abc import Mapping +from types import ModuleType +from typing import List + from .deprecation import deprecated, deprecation_warning, in_testing_environment from .errors import DeferredImportError @@ -312,6 +315,12 @@ def __init__( self._module = None self._available = None self._deferred_submodules = deferred_submodules + # If this import has a callback, then record this deferred + # import so that any direct imports of this module also trigger + # the resolution of this DeferredImportIndicator (and the + # corresponding callback) + if callback is not None: + DeferredImportCallbackFinder._callbacks.setdefault(name, []).append(self) def __bool__(self): self.resolve() @@ -433,6 +442,82 @@ def check_min_version(module, min_version): check_min_version._parser = None +# +# Note that we are duck-typing the Loader and MetaPathFinder base +# classes from importlib.abc. This avoids a (surprisingly costly) +# import of importlib.abc +# +class DeferredImportCallbackLoader: + """Custom Loader to resolve registered :py:class:`DeferredImportIndicator` objects + + This :py:class:`importlib.abc.Loader` loader wraps a regular loader + and automatically resolves the registered + :py:class:`DeferredImportIndicator` objects after the module is + loaded. + + """ + + def __init__(self, loader, deferred_indicators: List[DeferredImportIndicator]): + self._loader = loader + self._deferred_indicators = deferred_indicators + + def module_repr(self, module: ModuleType) -> str: + return self._loader.module_repr(module) + + def create_module(self, spec) -> ModuleType: + return self._loader.create_module(spec) + + def exec_module(self, module: ModuleType) -> None: + self._loader.exec_module(module) + # Now that the module has been loaded, trigger the resolution of + # the deferred indicators (and their associated callbacks) + for deferred in self._deferred_indicators: + deferred.resolve() + + def load_module(self, fullname) -> ModuleType: + return self._loader.load_module(fullname) + + +class DeferredImportCallbackFinder: + """Custom Finder that will wrap the normal loader to trigger callbacks + + This :py:class:`importlib.abc.MetaPathFinder` finder will wrap the + normal loader returned by ``PathFinder`` with a loader that will + trigger custom callbacks after the module is loaded. We use this to + trigger the post import callbacks registered through + :py:fcn:`attempt_import` even when a user imports the target library + directly (and not through attribute access on the + :py:class:`DeferredImportModule`. + + """ + _callbacks = {} + + def find_spec(self, fullname, path, target=None): + if fullname not in self._callbacks: + return None + + spec = importlib.machinery.PathFinder.find_spec(fullname, path, target) + if spec is None: + # Module not found. Returning None will proceed to the next + # finder (which is likely to raise a ModuleNotFoundError) + return None + spec.loader = DeferredImportCallbackLoader( + spec.loader, self._callbacks[fullname] + ) + return spec + + def invalidate_caches(self): + pass + + +_DeferredImportCallbackFinder = DeferredImportCallbackFinder() +# Insert the DeferredImportCallbackFinder at the beginning of the +# mata_path to that it is found before the standard finders (so that we +# can correctly inject the resolution of the DeferredImportIndicators -- +# which triggers the needed callbacks) +sys.meta_path.insert(0, _DeferredImportCallbackFinder) + + def attempt_import( name, error_message=None, diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index ba104203667..ca2ce0f9c6c 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -12,7 +12,6 @@ import logging import sys -from pyomo.common.dependencies import numpy_available from pyomo.common.deprecation import deprecated, relocated_module_attribute from pyomo.common.errors import TemplateExpressionError @@ -208,13 +207,6 @@ def check_if_numeric_type(obj): if obj_class in native_types: return obj_class in native_numeric_types - if 'numpy' in obj_class.__module__: - # trigger the resolution of numpy_available and check if this - # type was automatically registered - bool(numpy_available) - if obj_class in native_types: - return obj_class in native_numeric_types - try: obj_plus_0 = obj + 0 obj_p0_class = obj_plus_0.__class__ From 9205b81d6a3f39e2d1fc5c730deac932716b6a3a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:34:28 -0700 Subject: [PATCH 0539/1178] rename 'defer_check' to 'defer_import' --- doc/OnlineDocs/conf.py | 2 +- pyomo/common/dependencies.py | 31 ++++++++---- pyomo/common/tests/dep_mod.py | 4 +- pyomo/common/tests/deps.py | 5 +- pyomo/common/tests/test_dependencies.py | 48 +++++++++---------- pyomo/common/unittest.py | 2 +- pyomo/contrib/pynumero/dependencies.py | 2 +- .../examples/tests/test_cyipopt_examples.py | 2 +- pyomo/contrib/pynumero/intrinsic.py | 4 +- pyomo/core/base/units_container.py | 1 - pyomo/solvers/plugins/solvers/GAMS.py | 2 +- 11 files changed, 58 insertions(+), 45 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 04fe458407b..1aab4cd76c2 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -271,7 +271,7 @@ def check_output(self, want, got, optionflags): yaml_available, networkx_available, matplotlib_available, pympler_available, dill_available, ) -pint_available = attempt_import('pint', defer_check=False)[1] +pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available import pyomo.environ as _pe # (trigger all plugin registrations) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9aa6c4c4f7a..12ca6bd4ce3 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -130,7 +130,7 @@ class DeferredImportModule(object): This object is returned by :py:func:`attempt_import()` in lieu of the module when :py:func:`attempt_import()` is called with - ``defer_check=True``. Any attempts to access attributes on this + ``defer_import=True``. Any attempts to access attributes on this object will trigger the actual module import and return either the appropriate module attribute or else if the module import fails, raise a :py:class:`.DeferredImportError` exception. @@ -526,7 +526,8 @@ def attempt_import( alt_names=None, callback=None, importer=None, - defer_check=True, + defer_check=None, + defer_import=None, deferred_submodules=None, catch_exceptions=None, ): @@ -607,10 +608,16 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - If True (the default), then the attempted import is deferred - until the first use of either the module or the availability - flag. The method will return instances of :py:class:`DeferredImportModule` - and :py:class:`DeferredImportIndicator`. + DEPRECATED: renamed to ``defer_import`` + + defer_import: bool, optional + If True, then the attempted import is deferred until the first + use of either the module or the availability flag. The method + will return instances of :py:class:`DeferredImportModule` and + :py:class:`DeferredImportIndicator`. If False, the import will + be attempted immediately. If not set, then the import will be + deferred unless the ``name`` is already present in + ``sys.modules``. deferred_submodules: Iterable[str], optional If provided, an iterable of submodule names within this module @@ -661,9 +668,17 @@ def attempt_import( if catch_exceptions is None: catch_exceptions = (ImportError,) + if defer_check is not None: + deprecation_warning( + 'defer_check=%s is deprecated. Please use defer_import' % (defer_check,), + version='6.7.2.dev0', + ) + assert defer_import is None + defer_import = defer_check + # If we are going to defer the check until later, return the # deferred import module object - if defer_check: + if defer_import: if deferred_submodules: if isinstance(deferred_submodules, Mapping): deprecation_warning( @@ -706,7 +721,7 @@ def attempt_import( return DeferredImportModule(indicator, deferred, None), indicator if deferred_submodules: - raise ValueError("deferred_submodules is only valid if defer_check==True") + raise ValueError("deferred_submodules is only valid if defer_import==True") return _perform_import( name=name, diff --git a/pyomo/common/tests/dep_mod.py b/pyomo/common/tests/dep_mod.py index f6add596ed4..34c7219c6eb 100644 --- a/pyomo/common/tests/dep_mod.py +++ b/pyomo/common/tests/dep_mod.py @@ -13,8 +13,8 @@ __version__ = '1.5' -numpy, numpy_available = attempt_import('numpy', defer_check=True) +numpy, numpy_available = attempt_import('numpy', defer_import=True) bogus_nonexisting_module, bogus_nonexisting_module_available = attempt_import( - 'bogus_nonexisting_module', alt_names=['bogus_nem'], defer_check=True + 'bogus_nonexisting_module', alt_names=['bogus_nem'], defer_import=True ) diff --git a/pyomo/common/tests/deps.py b/pyomo/common/tests/deps.py index d00281553f4..5f8c1fffdf8 100644 --- a/pyomo/common/tests/deps.py +++ b/pyomo/common/tests/deps.py @@ -23,15 +23,16 @@ bogus_nonexisting_module_available as has_bogus_nem, ) -bogus, bogus_available = attempt_import('nonexisting.module.bogus', defer_check=True) +bogus, bogus_available = attempt_import('nonexisting.module.bogus', defer_import=True) pkl_test, pkl_available = attempt_import( - 'nonexisting.module.pickle_test', deferred_submodules=['submod'], defer_check=True + 'nonexisting.module.pickle_test', deferred_submodules=['submod'], defer_import=True ) pyo, pyo_available = attempt_import( 'pyomo', alt_names=['pyo'], + defer_import=True, deferred_submodules={'version': None, 'common.tests.dep_mod': ['dm']}, ) diff --git a/pyomo/common/tests/test_dependencies.py b/pyomo/common/tests/test_dependencies.py index 30822a4f81f..31f9520b613 100644 --- a/pyomo/common/tests/test_dependencies.py +++ b/pyomo/common/tests/test_dependencies.py @@ -45,7 +45,7 @@ def test_import_error(self): module_obj, module_available = attempt_import( '__there_is_no_module_named_this__', 'Testing import of a non-existent module', - defer_check=False, + defer_import=False, ) self.assertFalse(module_available) with self.assertRaisesRegex( @@ -85,7 +85,7 @@ def test_pickle(self): def test_import_success(self): module_obj, module_available = attempt_import( - 'ply', 'Testing import of ply', defer_check=False + 'ply', 'Testing import of ply', defer_import=False ) self.assertTrue(module_available) import ply @@ -123,7 +123,7 @@ def test_imported_deferred_import(self): def test_min_version(self): mod, avail = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='1.0', defer_check=False + 'pyomo.common.tests.dep_mod', minimum_version='1.0', defer_import=False ) self.assertTrue(avail) self.assertTrue(inspect.ismodule(mod)) @@ -131,7 +131,7 @@ def test_min_version(self): self.assertFalse(check_min_version(mod, '2.0')) mod, avail = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_check=False + 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_import=False ) self.assertFalse(avail) self.assertIs(type(mod), ModuleUnavailable) @@ -146,7 +146,7 @@ def test_min_version(self): 'pyomo.common.tests.dep_mod', error_message="Failed import", minimum_version='2.0', - defer_check=False, + defer_import=False, ) self.assertFalse(avail) self.assertIs(type(mod), ModuleUnavailable) @@ -159,10 +159,10 @@ def test_min_version(self): # Verify check_min_version works with deferred imports - mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) self.assertTrue(check_min_version(mod, '1.0')) - mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) self.assertFalse(check_min_version(mod, '2.0')) # Verify check_min_version works when called directly @@ -174,10 +174,10 @@ def test_min_version(self): self.assertFalse(check_min_version(mod, '1.0')) def test_and_or(self): - mod0, avail0 = attempt_import('ply', defer_check=True) - mod1, avail1 = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod0, avail0 = attempt_import('ply', defer_import=True) + mod1, avail1 = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) mod2, avail2 = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_check=True + 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_import=True ) _and = avail0 & avail1 @@ -233,11 +233,11 @@ def test_callbacks(self): def _record_avail(module, avail): ans.append(avail) - mod0, avail0 = attempt_import('ply', defer_check=True, callback=_record_avail) + mod0, avail0 = attempt_import('ply', defer_import=True, callback=_record_avail) mod1, avail1 = attempt_import( 'pyomo.common.tests.dep_mod', minimum_version='2.0', - defer_check=True, + defer_import=True, callback=_record_avail, ) @@ -250,7 +250,7 @@ def _record_avail(module, avail): def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=True, ) with self.assertRaisesRegex(ValueError, "cannot import module"): @@ -260,7 +260,7 @@ def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) self.assertFalse(avail) @@ -268,7 +268,7 @@ def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, catch_exceptions=(ImportError, ValueError), ) self.assertFalse(avail) @@ -280,7 +280,7 @@ def test_import_exceptions(self): ): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=True, catch_exceptions=(ImportError,), ) @@ -288,7 +288,7 @@ def test_import_exceptions(self): def test_generate_warning(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) @@ -324,7 +324,7 @@ def test_generate_warning(self): def test_log_warning(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) log = StringIO() @@ -366,9 +366,9 @@ def test_importer(self): def _importer(): attempted_import.append(True) - return attempt_import('pyomo.common.tests.dep_mod', defer_check=False)[0] + return attempt_import('pyomo.common.tests.dep_mod', defer_import=False)[0] - mod, avail = attempt_import('foo', importer=_importer, defer_check=True) + mod, avail = attempt_import('foo', importer=_importer, defer_import=True) self.assertEqual(attempted_import, []) self.assertIsInstance(mod, DeferredImportModule) @@ -401,17 +401,17 @@ def test_deferred_submodules(self): self.assertTrue(inspect.ismodule(deps.dm)) with self.assertRaisesRegex( - ValueError, "deferred_submodules is only valid if defer_check==True" + ValueError, "deferred_submodules is only valid if defer_import==True" ): mod, mod_available = attempt_import( 'nonexisting.module', - defer_check=False, + defer_import=False, deferred_submodules={'submod': None}, ) mod, mod_available = attempt_import( 'nonexisting.module', - defer_check=True, + defer_import=True, deferred_submodules={'submod.subsubmod': None}, ) self.assertIs(type(mod), DeferredImportModule) @@ -427,7 +427,7 @@ def test_UnavailableClass(self): module_obj, module_available = attempt_import( '__there_is_no_module_named_this__', 'Testing import of a non-existent module', - defer_check=False, + defer_import=False, ) class A_Class(UnavailableClass(module_obj)): diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 9a21b35faa8..84b44775c1b 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -631,7 +631,7 @@ def initialize_dependencies(self): cls.package_modules = {} packages_used = set(sum(list(cls.package_dependencies.values()), [])) for package_ in packages_used: - pack, pack_avail = attempt_import(package_, defer_check=False) + pack, pack_avail = attempt_import(package_, defer_import=False) cls.package_available[package_] = pack_avail cls.package_modules[package_] = pack diff --git a/pyomo/contrib/pynumero/dependencies.py b/pyomo/contrib/pynumero/dependencies.py index 9e2088ffa0a..d323bd43e84 100644 --- a/pyomo/contrib/pynumero/dependencies.py +++ b/pyomo/contrib/pynumero/dependencies.py @@ -17,7 +17,7 @@ 'numpy', 'Pynumero requires the optional Pyomo dependency "numpy"', minimum_version='1.13.0', - defer_check=False, + defer_import=False, ) if not numpy_available: diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 1f45f26d43b..408a0197382 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -35,7 +35,7 @@ 'One of the tests below requires a recent version of pandas for' ' comparing with a tolerance.', minimum_version='1.1.0', - defer_check=False, + defer_import=False, ) from pyomo.contrib.pynumero.asl import AmplInterface diff --git a/pyomo/contrib/pynumero/intrinsic.py b/pyomo/contrib/pynumero/intrinsic.py index 84675cc4c02..34054e7ffa2 100644 --- a/pyomo/contrib/pynumero/intrinsic.py +++ b/pyomo/contrib/pynumero/intrinsic.py @@ -11,9 +11,7 @@ from pyomo.common.dependencies import numpy as np, attempt_import -block_vector = attempt_import( - 'pyomo.contrib.pynumero.sparse.block_vector', defer_check=True -)[0] +block_vector = attempt_import('pyomo.contrib.pynumero.sparse.block_vector')[0] def norm(x, ord=None): diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index 1bf25ffdead..fb3d28385d0 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -127,7 +127,6 @@ pint_module, pint_available = attempt_import( 'pint', - defer_check=True, error_message=( 'The "pint" package failed to import. ' 'This package is necessary to use Pyomo units.' diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index e84cbdb441d..be3499a2f6b 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -41,7 +41,7 @@ from pyomo.common.dependencies import attempt_import -gdxcc, gdxcc_available = attempt_import('gdxcc', defer_check=True) +gdxcc, gdxcc_available = attempt_import('gdxcc') logger = logging.getLogger('pyomo.solvers') From 4f1b93c0f08b5819b2f54c565b2ebc8c96061887 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:34:44 -0700 Subject: [PATCH 0540/1178] NFC: update comment --- pyomo/common/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 12ca6bd4ce3..8246cbb0776 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -597,7 +597,7 @@ def attempt_import( module in the ``globals()`` namespaces. For example, the alt_names for NumPy would be ``['np']``. (deprecated in version 6.0) - callback: function, optional + callback: Callable[[ModuleType, bool], None], optional A function with the signature "``fcn(module, available)``" that will be called after the import is first attempted. From d80cde1728883cb11e193be4dbb1109e68833665 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:35:22 -0700 Subject: [PATCH 0541/1178] Do not defer import if module is already imported --- pyomo/common/dependencies.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 8246cbb0776..9b2f5ab5767 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -676,6 +676,15 @@ def attempt_import( assert defer_import is None defer_import = defer_check + # If the module has already been imported, there is no reason to + # further defer things: just import it. + if defer_import is None: + if name in sys.modules: + defer_import = False + deferred_submodules = None + else: + defer_import = True + # If we are going to defer the check until later, return the # deferred import module object if defer_import: From 6e2db622df1e3e556be9aee6523d269dd3bf95df Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:37:48 -0700 Subject: [PATCH 0542/1178] NFC: apply black --- pyomo/common/dependencies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9b2f5ab5767..5bc752deb53 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -490,6 +490,7 @@ class DeferredImportCallbackFinder: :py:class:`DeferredImportModule`. """ + _callbacks = {} def find_spec(self, fullname, path, target=None): From a10d36405c28cabb180dae9c852335f7dd967e95 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:41:11 -0700 Subject: [PATCH 0543/1178] NFC: fix typo --- pyomo/common/dependencies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 5bc752deb53..9034342b5a1 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -513,9 +513,9 @@ def invalidate_caches(self): _DeferredImportCallbackFinder = DeferredImportCallbackFinder() # Insert the DeferredImportCallbackFinder at the beginning of the -# mata_path to that it is found before the standard finders (so that we -# can correctly inject the resolution of the DeferredImportIndicators -- -# which triggers the needed callbacks) +# sys.meta_path to that it is found before the standard finders (so that +# we can correctly inject the resolution of the DeferredImportIndicators +# -- which triggers the needed callbacks) sys.meta_path.insert(0, _DeferredImportCallbackFinder) From 648aae7392ec98bc33debe94aaa530b7f872417a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 00:00:10 -0700 Subject: [PATCH 0544/1178] Update type registration tests to reflect automatic numpy callback --- pyomo/core/tests/unit/test_numvalue.py | 90 ++++++++++++++++++++------ 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index bd784d655e8..2dca2df56a6 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -50,7 +50,16 @@ def __init__(self, val=0): class MyBogusNumericType(MyBogusType): def __add__(self, other): - return MyBogusNumericType(self.val + float(other)) + if other.__class__ in native_numeric_types: + return MyBogusNumericType(self.val + float(other)) + else: + return NotImplemented + + def __le__(self, other): + if other.__class__ in native_numeric_types: + return self.val <= float(other) + else: + return NotImplemented def __lt__(self, other): return self.val < float(other) @@ -534,6 +543,8 @@ def test_unknownNumericType(self): try: val = as_numeric(ref) self.assertEqual(val().val, 42.0) + self.assertIn(MyBogusNumericType, native_numeric_types) + self.assertIn(MyBogusNumericType, native_types) finally: native_numeric_types.remove(MyBogusNumericType) native_types.remove(MyBogusNumericType) @@ -562,10 +573,43 @@ def test_numpy_basic_bool_registration(self): @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_automatic_numpy_registration(self): cmd = ( - 'import pyomo; from pyomo.core.base import Var, Param; ' - 'from pyomo.core.base.units_container import units; import numpy as np; ' - 'print(np.float64 in pyomo.common.numeric_types.native_numeric_types); ' - '%s; print(np.float64 in pyomo.common.numeric_types.native_numeric_types)' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + 'print("float64" in [_.__name__ for _ in nnt]); ' + 'import numpy; ' + 'print("float64" in [_.__name__ for _ in nnt])' + ) + + rc = subprocess.run( + [sys.executable, '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual((rc.returncode, rc.stdout), (0, "False\nTrue\n")) + + cmd = ( + 'import numpy; ' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + 'print("float64" in [_.__name__ for _ in nnt])' + ) + + rc = subprocess.run( + [sys.executable, '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual((rc.returncode, rc.stdout), (0, "True\n")) + + def test_unknownNumericType_expr_registration(self): + cmd = ( + 'import pyomo; ' + 'from pyomo.core.base import Var, Param; ' + 'from pyomo.core.base.units_container import units; ' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + f'from {__name__} import MyBogusNumericType; ' + 'ref = MyBogusNumericType(42); ' + 'print(MyBogusNumericType in nnt); %s; print(MyBogusNumericType in nnt); ' ) def _tester(expr): @@ -575,19 +619,29 @@ def _tester(expr): stderr=subprocess.STDOUT, text=True, ) - self.assertEqual((rc.returncode, rc.stdout), (0, "False\nTrue\n")) - - _tester('Var() <= np.float64(5)') - _tester('np.float64(5) <= Var()') - _tester('np.float64(5) + Var()') - _tester('Var() + np.float64(5)') - _tester('v = Var(); v.construct(); v.value = np.float64(5)') - _tester('p = Param(mutable=True); p.construct(); p.value = np.float64(5)') - _tester('v = Var(units=units.m); v.construct(); v.value = np.float64(5)') - _tester( - 'p = Param(mutable=True, units=units.m); p.construct(); ' - 'p.value = np.float64(5)' - ) + self.assertEqual( + (rc.returncode, rc.stdout), + ( + 0, + '''False +WARNING: Dynamically registering the following numeric type: + pyomo.core.tests.unit.test_numvalue.MyBogusNumericType + Dynamic registration is supported for convenience, but there are known + limitations to this approach. We recommend explicitly registering numeric + types using RegisterNumericType() or RegisterIntegerType(). +True +''', + ), + ) + + _tester('Var() <= ref') + _tester('ref <= Var()') + _tester('ref + Var()') + _tester('Var() + ref') + _tester('v = Var(); v.construct(); v.value = ref') + _tester('p = Param(mutable=True); p.construct(); p.value = ref') + _tester('v = Var(units=units.m); v.construct(); v.value = ref') + _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') if __name__ == "__main__": From adbf1de2e3b6ca8c16eb6b81a9ae4966ea30fd94 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 00:09:19 -0700 Subject: [PATCH 0545/1178] Remove numpy reference/check --- pyomo/common/numeric_types.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 412a1bbeade..616d4c4bae4 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -228,13 +228,6 @@ def check_if_logical_type(obj): if obj_class in native_types: return obj_class in native_logical_types - if 'numpy' in obj_class.__module__: - # trigger the resolution of numpy_available and check if this - # type was automatically registered - bool(numpy_available) - if obj_class in native_types: - return obj_class in native_logical_types - try: if all( ( From ea77dca9ecf302e3757fbc916ce515562e16473e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 12:36:36 -0700 Subject: [PATCH 0546/1178] Resolve registration for modules that were already inmported --- pyomo/common/dependencies.py | 117 +++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9034342b5a1..505211aeb56 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -822,20 +822,36 @@ def declare_deferred_modules_as_importable(globals_dict): :py:class:`ModuleUnavailable` instance. """ - _global_name = globals_dict['__name__'] + '.' - deferred = list( - (k, v) for k, v in globals_dict.items() if type(v) is DeferredImportModule - ) - while deferred: - name, mod = deferred.pop(0) - mod.__path__ = None - mod.__spec__ = None - sys.modules[_global_name + name] = mod - deferred.extend( - (name + '.' + k, v) - for k, v in mod.__dict__.items() - if type(v) is DeferredImportModule - ) + return declare_modules_as_importable(globals_dict).__exit__(None, None, None) + + +class declare_modules_as_importable(object): + def __init__(self, globals_dict): + self.globals_dict = globals_dict + self.init_dict = {} + + def __enter__(self): + self.init_dict.update(self.globals_dict) + + def __exit__(self, exc_type, exc_value, traceback): + _global_name = self.globals_dict['__name__'] + '.' + deferred = [ + (k, v) + for k, v in self.globals_dict.items() + if k not in self.init_dict + and isinstance(v, (ModuleType, DeferredImportModule)) + ] + while deferred: + name, mod = deferred.pop(0) + mod.__path__ = None + mod.__spec__ = None + sys.modules[_global_name + name] = mod + if isinstance(mod, DeferredImportModule): + deferred.extend( + (name + '.' + k, v) + for k, v in mod.__dict__.items() + if type(v) is DeferredImportModule + ) # @@ -952,41 +968,48 @@ def _pyutilib_importer(): return importlib.import_module('pyutilib') -# Standard libraries that are slower to import and not strictly required -# on all platforms / situations. -ctypes, _ = attempt_import( - 'ctypes', deferred_submodules=['util'], callback=_finalize_ctypes -) -random, _ = attempt_import('random') - -# Commonly-used optional dependencies -dill, dill_available = attempt_import('dill') -mpi4py, mpi4py_available = attempt_import('mpi4py') -networkx, networkx_available = attempt_import('networkx') -numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) -pandas, pandas_available = attempt_import('pandas') -plotly, plotly_available = attempt_import('plotly') -pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) -pyutilib, pyutilib_available = attempt_import('pyutilib', importer=_pyutilib_importer) -scipy, scipy_available = attempt_import( - 'scipy', - callback=_finalize_scipy, - deferred_submodules=['stats', 'sparse', 'spatial', 'integrate'], -) -yaml, yaml_available = attempt_import('yaml', callback=_finalize_yaml) - -# Note that matplotlib.pyplot can generate a runtime error on OSX when -# not installed as a Framework (as is the case in the CI systems) -matplotlib, matplotlib_available = attempt_import( - 'matplotlib', - callback=_finalize_matplotlib, - deferred_submodules=['pyplot', 'pylab'], - catch_exceptions=(ImportError, RuntimeError), -) +# +# Note: because we will be calling +# declare_deferred_modules_as_importable, it is important that the +# following declarations explicitly defer_import (even if the target +# module has already been imported) +# +with declare_modules_as_importable(globals()): + # Standard libraries that are slower to import and not strictly required + # on all platforms / situations. + ctypes, _ = attempt_import( + 'ctypes', deferred_submodules=['util'], callback=_finalize_ctypes + ) + random, _ = attempt_import('random') + + # Commonly-used optional dependencies + dill, dill_available = attempt_import('dill') + mpi4py, mpi4py_available = attempt_import('mpi4py') + networkx, networkx_available = attempt_import('networkx') + numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) + pandas, pandas_available = attempt_import('pandas') + plotly, plotly_available = attempt_import('plotly') + pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) + pyutilib, pyutilib_available = attempt_import( + 'pyutilib', importer=_pyutilib_importer + ) + scipy, scipy_available = attempt_import( + 'scipy', + callback=_finalize_scipy, + deferred_submodules=['stats', 'sparse', 'spatial', 'integrate'], + ) + yaml, yaml_available = attempt_import('yaml', callback=_finalize_yaml) + + # Note that matplotlib.pyplot can generate a runtime error on OSX when + # not installed as a Framework (as is the case in the CI systems) + matplotlib, matplotlib_available = attempt_import( + 'matplotlib', + callback=_finalize_matplotlib, + deferred_submodules=['pyplot', 'pylab'], + catch_exceptions=(ImportError, RuntimeError), + ) try: import cPickle as pickle except ImportError: import pickle - -declare_deferred_modules_as_importable(globals()) From 24f649334eb0d2291551c8b09923e251e7ef486c Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 27 Feb 2024 12:48:51 -0800 Subject: [PATCH 0547/1178] clean up, moved _expand_indexed_unknowns --- pyomo/contrib/parmest/parmest.py | 75 ++++++++++++-------------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 9e5b480332d..ffe9afc059e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -226,12 +226,12 @@ def _experiment_instance_creation_callback( thetavals = outer_cb_data["ThetaVals"] # dlw august 2018: see mea code for more general theta - for vstr in thetavals: - theta_cuid = ComponentUID(vstr) + for name, val in thetavals.items(): + theta_cuid = ComponentUID(name) theta_object = theta_cuid.find_component_on(instance) - if thetavals[vstr] is not None: + if val is not None: # print("Fixing",vstr,"at",str(thetavals[vstr])) - theta_object.fix(thetavals[vstr]) + theta_object.fix(val) else: # print("Freeing",vstr) theta_object.unfix() @@ -400,6 +400,29 @@ def _return_theta_names(self): self.estimator_theta_names ) # default theta_names, created when Estimator object is created + def _expand_indexed_unknowns(self, model_temp): + """ + Expand indexed variables to get full list of thetas + """ + model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] + + # check for indexed theta items + indexed_theta_list = [] + for theta_i in model_theta_list: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + for ind in var_validate.index_set(): + if ind is not None: + indexed_theta_list.append(theta_i + '[' + str(ind) + ']') + else: + indexed_theta_list.append(theta_i) + + # if we found indexed thetas, use expanded list + if len(indexed_theta_list) > len(model_theta_list): + model_theta_list = indexed_theta_list + + return model_theta_list + def _create_parmest_model(self, experiment_number): """ Modify the Pyomo model for parameter estimation @@ -1155,28 +1178,6 @@ def leaveNout_bootstrap_test( return results - # expand indexed variables to get full list of thetas - def _expand_indexed_unknowns(self, model_temp): - - model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] - - # check for indexed theta items - indexed_theta_list = [] - for theta_i in model_theta_list: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - for ind in var_validate.index_set(): - if ind is not None: - indexed_theta_list.append(theta_i + '[' + str(ind) + ']') - else: - indexed_theta_list.append(theta_i) - - # if we found indexed thetas, use expanded list - if len(indexed_theta_list) > len(model_theta_list): - model_theta_list = indexed_theta_list - - return model_theta_list - def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ Objective value for each theta @@ -1212,28 +1213,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) - # # iterate over original theta_names - # for theta_i in self.theta_names: - # var_cuid = ComponentUID(theta_i) - # var_validate = var_cuid.find_component_on(model_temp) - # # check if theta in theta_names are indexed - # try: - # # get component UID of Set over which theta is defined - # set_cuid = ComponentUID(var_validate.index_set()) - # # access and iterate over the Set to generate theta names as they appear - # # in the pyomo model - # set_validate = set_cuid.find_component_on(model_temp) - # for s in set_validate: - # self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" - # # generate list of theta names - # model_theta_list.append(self_theta_temp) - # # if theta is not indexed, copy theta name to list as-is - # except AttributeError: - # self_theta_temp = repr(var_cuid) - # model_theta_list.append(self_theta_temp) - # except: - # raise - # if self.theta_names is not the same as temp model_theta_list, # create self.theta_names_updated if set(self.estimator_theta_names) == set(model_theta_list) and len( From 6699fde39928e0dc9eba0482bc018a8d1a540101 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:07:43 -0700 Subject: [PATCH 0548/1178] declare_modules_as_importable will also detect imported submodules --- pyomo/common/dependencies.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 505211aeb56..2954a8bff83 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -829,25 +829,31 @@ class declare_modules_as_importable(object): def __init__(self, globals_dict): self.globals_dict = globals_dict self.init_dict = {} + self.init_modules = None def __enter__(self): self.init_dict.update(self.globals_dict) + self.init_modules = set(sys.modules) def __exit__(self, exc_type, exc_value, traceback): _global_name = self.globals_dict['__name__'] + '.' - deferred = [ - (k, v) + deferred = { + k: v for k, v in self.globals_dict.items() if k not in self.init_dict and isinstance(v, (ModuleType, DeferredImportModule)) - ] + } + if self.init_modules: + for name in set(sys.modules) - self.init_modules: + if '.' in name and name.split('.', 1)[0] in deferred: + sys.modules[_global_name + name] = sys.modules[name] while deferred: - name, mod = deferred.pop(0) + name, mod = deferred.popitem() mod.__path__ = None mod.__spec__ = None sys.modules[_global_name + name] = mod if isinstance(mod, DeferredImportModule): - deferred.extend( + deferred.update( (name + '.' + k, v) for k, v in mod.__dict__.items() if type(v) is DeferredImportModule From a95af93eb62be5ea0d4137457f904d109de00363 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:08:05 -0700 Subject: [PATCH 0549/1178] Update docs, deprecate declare_deferred_modules_as_importable --- pyomo/common/dependencies.py | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 2954a8bff83..7d5437f6da9 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -782,6 +782,11 @@ def _perform_import( return module, False +@deprecated( + "declare_deferred_modules_as_importable() is dperecated. " + "Use the declare_modules_as_importable() context manager." + version='6.7.2.dev0' +) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable @@ -826,6 +831,50 @@ def declare_deferred_modules_as_importable(globals_dict): class declare_modules_as_importable(object): + """Make all :py:class:`ModuleType` and :py:class:`DeferredImportModules` + importable through the ``globals_dict`` context. + + This context manager will detect all modules imported into the + specified ``globals_dict`` environment (either directly or through + :py:fcn:`attempt_import`) and will make those modules importable + from the specified ``globals_dict`` context. It works by detecting + changes in the specified ``globals_dict`` dictionary and adding any new + modules or instances of :py:class:`DeferredImportModule` that it + finds (and any of their deferred submodules) to ``sys.modules`` so + that the modules can be imported through the ``globals_dict`` + namespace. + + For example, ``pyomo/common/dependencies.py`` declares: + + .. doctest:: + :hide: + + >>> from pyomo.common.dependencies import ( + ... attempt_import, _finalize_scipy, __dict__ as dep_globals, + ... declare_deferred_modules_as_importable, ) + >>> # Sphinx does not provide a proper globals() + >>> def globals(): return dep_globals + + .. doctest:: + + >>> with declare_modules_as_importable(globals()): + ... scipy, scipy_available = attempt_import( + ... 'scipy', callback=_finalize_scipy, + ... deferred_submodules=['stats', 'sparse', 'spatial', 'integrate']) + + Which enables users to use: + + .. doctest:: + + >>> import pyomo.common.dependencies.scipy.sparse as spa + + If the deferred import has not yet been triggered, then the + :py:class:`DeferredImportModule` is returned and named ``spa``. + However, if the import has already been triggered, then ``spa`` will + either be the ``scipy.sparse`` module, or a + :py:class:`ModuleUnavailable` instance. + + """ def __init__(self, globals_dict): self.globals_dict = globals_dict self.init_dict = {} From 076dabe721876e931cb488e84f9b546e4a9baa2f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:08:27 -0700 Subject: [PATCH 0550/1178] Add deep import for numpy to resolve scipy import error --- pyomo/common/dependencies.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 7d5437f6da9..aab0d55d9b4 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -964,6 +964,11 @@ def _finalize_matplotlib(module, available): def _finalize_numpy(np, available): if not available: return + # scipy has a dependence on numpy.testing, and if we don't import it + # as part of resolving numpy, then certain deferred scipy imports + # fail when run under pytest. + import numpy.testing + from . import numeric_types # Register ndarray as a native type to prevent 1-element ndarrays From dc19d4e333f6c714deab75a06e9602c90ea97b5a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:09:49 -0700 Subject: [PATCH 0551/1178] Fix typo --- pyomo/common/dependencies.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index aab0d55d9b4..900618b696a 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -784,8 +784,8 @@ def _perform_import( @deprecated( "declare_deferred_modules_as_importable() is dperecated. " - "Use the declare_modules_as_importable() context manager." - version='6.7.2.dev0' + "Use the declare_modules_as_importable() context manager.", + version='6.7.2.dev0', ) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable @@ -875,6 +875,7 @@ class declare_modules_as_importable(object): :py:class:`ModuleUnavailable` instance. """ + def __init__(self, globals_dict): self.globals_dict = globals_dict self.init_dict = {} From 6efece0f3385b5e782525d0d7a1aeee45f824039 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:30:21 -0700 Subject: [PATCH 0552/1178] Resolve doctest failures --- doc/OnlineDocs/contributed_packages/pyros.rst | 2 +- pyomo/common/dependencies.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index aad37a9685a..76a751dd994 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -689,7 +689,7 @@ could have been equivalently written as: ... }, ... ) ============================================================================== - PyROS: The Pyomo Robust Optimization Solver. + PyROS: The Pyomo Robust Optimization Solver... ... ------------------------------------------------------------------------------ Robust optimal solution identified. diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 900618b696a..c09594b6e12 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -813,6 +813,7 @@ def declare_deferred_modules_as_importable(globals_dict): ... 'scipy', callback=_finalize_scipy, ... deferred_submodules=['stats', 'sparse', 'spatial', 'integrate']) >>> declare_deferred_modules_as_importable(globals()) + WARNING: DEPRECATED: ... Which enables users to use: @@ -851,7 +852,7 @@ class declare_modules_as_importable(object): >>> from pyomo.common.dependencies import ( ... attempt_import, _finalize_scipy, __dict__ as dep_globals, - ... declare_deferred_modules_as_importable, ) + ... declare_modules_as_importable, ) >>> # Sphinx does not provide a proper globals() >>> def globals(): return dep_globals From 754de4114ac58381089c21fdffa0ee3345b8d21a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:30:48 -0700 Subject: [PATCH 0553/1178] NFC: doc updates --- pyomo/common/dependencies.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index c09594b6e12..f0713a53cbf 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -485,7 +485,7 @@ class DeferredImportCallbackFinder: normal loader returned by ``PathFinder`` with a loader that will trigger custom callbacks after the module is loaded. We use this to trigger the post import callbacks registered through - :py:fcn:`attempt_import` even when a user imports the target library + :py:func:`attempt_import` even when a user imports the target library directly (and not through attribute access on the :py:class:`DeferredImportModule`. @@ -582,7 +582,8 @@ def attempt_import( The message for the exception raised by :py:class:`ModuleUnavailable` only_catch_importerror: bool, optional - DEPRECATED: use catch_exceptions instead or only_catch_importerror. + DEPRECATED: use ``catch_exceptions`` instead of ``only_catch_importerror``. + If True (the default), exceptions other than ``ImportError`` raised during module import will be reraised. If False, any exception will result in returning a :py:class:`ModuleUnavailable` object. @@ -593,13 +594,14 @@ def attempt_import( ``module.__version__``) alt_names: list, optional - DEPRECATED: alt_names no longer needs to be specified and is ignored. + DEPRECATED: ``alt_names`` no longer needs to be specified and is ignored. + A list of common alternate names by which to look for this module in the ``globals()`` namespaces. For example, the alt_names for NumPy would be ``['np']``. (deprecated in version 6.0) callback: Callable[[ModuleType, bool], None], optional - A function with the signature "``fcn(module, available)``" that + A function with the signature ``fcn(module, available)`` that will be called after the import is first attempted. importer: function, optional @@ -609,7 +611,7 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - DEPRECATED: renamed to ``defer_import`` + DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2.dev0) defer_import: bool, optional If True, then the attempted import is deferred until the first @@ -783,8 +785,8 @@ def _perform_import( @deprecated( - "declare_deferred_modules_as_importable() is dperecated. " - "Use the declare_modules_as_importable() context manager.", + "``declare_deferred_modules_as_importable()`` is deprecated. " + "Use the :py:class:`declare_modules_as_importable` context manager.", version='6.7.2.dev0', ) def declare_deferred_modules_as_importable(globals_dict): @@ -837,7 +839,7 @@ class declare_modules_as_importable(object): This context manager will detect all modules imported into the specified ``globals_dict`` environment (either directly or through - :py:fcn:`attempt_import`) and will make those modules importable + :py:func:`attempt_import`) and will make those modules importable from the specified ``globals_dict`` context. It works by detecting changes in the specified ``globals_dict`` dictionary and adding any new modules or instances of :py:class:`DeferredImportModule` that it From 96658623579fb767c4a6347c01c6392e17ebf774 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:31:07 -0700 Subject: [PATCH 0554/1178] Add backends tot eh matplotlib deferred imports --- pyomo/common/dependencies.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index f0713a53cbf..edf32baa6d6 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -963,6 +963,8 @@ def _finalize_matplotlib(module, available): if in_testing_environment(): module.use('Agg') import matplotlib.pyplot + import matplotlib.pylab + import matplotlib.backends def _finalize_numpy(np, available): @@ -1069,7 +1071,7 @@ def _pyutilib_importer(): matplotlib, matplotlib_available = attempt_import( 'matplotlib', callback=_finalize_matplotlib, - deferred_submodules=['pyplot', 'pylab'], + deferred_submodules=['pyplot', 'pylab', 'backends'], catch_exceptions=(ImportError, RuntimeError), ) From 0938052a92a147e0f450e8744fb400eacae89f5e Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 27 Feb 2024 19:14:46 -0500 Subject: [PATCH 0555/1178] Simplify a few config domain validators --- pyomo/contrib/pyros/config.py | 77 ++++++------------------ pyomo/contrib/pyros/tests/test_config.py | 26 +++++--- 2 files changed, 36 insertions(+), 67 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index a7ca41d095f..bc2bfd591e6 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -26,71 +26,34 @@ default_pyros_solver_logger = setup_pyros_logger() -class LoggerType: +def logger_domain(obj): """ - Domain validator for objects castable to logging.Logger. - """ - - def __call__(self, obj): - """ - Cast object to logger. + Domain validator for logger-type arguments. - Parameters - ---------- - obj : object - Object to be cast. + This admits any object of type ``logging.Logger``, + or which can be cast to ``logging.Logger``. + """ + if isinstance(obj, logging.Logger): + return obj + else: + return logging.getLogger(obj) - Returns - ------- - logging.Logger - If `str_or_logger` is of type `logging.Logger`,then - `str_or_logger` is returned. - Otherwise, ``logging.getLogger(str_or_logger)`` - is returned. - """ - if isinstance(obj, logging.Logger): - return obj - else: - return logging.getLogger(obj) - def domain_name(self): - """Return str briefly describing domain encompassed by self.""" - return "None, str or logging.Logger" +logger_domain.domain_name = "None, str or logging.Logger" -class PositiveIntOrMinusOne: +def positive_int_or_minus_one(obj): """ - Domain validator for objects castable to a - strictly positive int or -1. + Domain validator for objects castable to a strictly + positive int or -1. """ + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError(f"Expected positive int or -1, but received value {obj!r}") + return ans - def __call__(self, obj): - """ - Cast object to positive int or -1. - Parameters - ---------- - obj : object - Object of interest. - - Returns - ------- - int - Positive int, or -1. - - Raises - ------ - ValueError - If object not castable to positive int, or -1. - """ - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError(f"Expected positive int or -1, but received value {obj!r}") - return ans - - def domain_name(self): - """Return str briefly describing domain encompassed by self.""" - return "positive int or -1" +positive_int_or_minus_one.domain_name = "positive int or -1" def mutable_param_validator(param_obj): @@ -721,7 +684,7 @@ def pyros_config(): "max_iter", ConfigValue( default=-1, - domain=PositiveIntOrMinusOne(), + domain=positive_int_or_minus_one, description=( """ Iteration limit. If -1 is provided, then no iteration @@ -766,7 +729,7 @@ def pyros_config(): "progress_logger", ConfigValue( default=default_pyros_solver_logger, - domain=LoggerType(), + domain=logger_domain, doc=( """ Logger (or name thereof) used for reporting PyROS solver diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 76b9114b9e6..3555391fd95 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -12,9 +12,9 @@ from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, - LoggerType, + logger_domain, SolverNotResolvable, - PositiveIntOrMinusOne, + positive_int_or_minus_one, pyros_config, SolverIterable, SolverResolvable, @@ -557,16 +557,22 @@ def test_positive_int_or_minus_one(self): """ Test positive int or -1 validator works as expected. """ - standardizer_func = PositiveIntOrMinusOne() + standardizer_func = positive_int_or_minus_one self.assertIs( standardizer_func(1.0), 1, - msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), + msg=( + f"{positive_int_or_minus_one.__name__} " + "does not standardize as expected." + ), ) self.assertEqual( standardizer_func(-1.00), -1, - msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), + msg=( + f"{positive_int_or_minus_one.__name__} " + "does not standardize as expected." + ), ) exc_str = r"Expected positive int or -1, but received value.*" @@ -576,26 +582,26 @@ def test_positive_int_or_minus_one(self): standardizer_func(0) -class TestLoggerType(unittest.TestCase): +class TestLoggerDomain(unittest.TestCase): """ - Test logger type validator. + Test logger type domain validator. """ def test_logger_type(self): """ Test logger type validator. """ - standardizer_func = LoggerType() + standardizer_func = logger_domain mylogger = logging.getLogger("example") self.assertIs( standardizer_func(mylogger), mylogger, - msg=f"{LoggerType.__name__} output not as expected", + msg=f"{standardizer_func.__name__} output not as expected", ) self.assertIs( standardizer_func(mylogger.name), mylogger, - msg=f"{LoggerType.__name__} output not as expected", + msg=f"{standardizer_func.__name__} output not as expected", ) exc_str = r"A logger name must be a string" From 3d5cb6c8fc7eae46f91d85bc4bf2cc71aaac9dc9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 27 Feb 2024 20:22:14 -0500 Subject: [PATCH 0556/1178] Fix PyROS discrete separation iteration log --- .../contrib/pyros/pyros_algorithm_methods.py | 2 +- .../pyros/separation_problem_methods.py | 1 + pyomo/contrib/pyros/solve_data.py | 29 +++++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 45b652447ff..f0e32a284bb 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -805,7 +805,7 @@ def ROSolver_iterative_solve(model_data, config): len(scaled_violations) == len(separation_model.util.performance_constraints) and not separation_results.subsolver_error and not separation_results.time_out - ) + ) or separation_results.all_discrete_scenarios_exhausted iter_log_record = IterationLogRecord( iteration=k, diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 084b0442ae6..b5939ff5b19 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -649,6 +649,7 @@ def perform_separation_loop(model_data, config, solve_globally): solver_call_results=ComponentMap(), solved_globally=solve_globally, worst_case_perf_con=None, + all_discrete_scenarios_exhausted=True, ) perf_con_to_maximize = sorted_priority_groups[ diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index bc6c071c9a3..c31eb8e5d3f 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -347,16 +347,23 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or int, optional + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. + all_discrete_scenarios_exhausted : bool, optional + For problems with discrete uncertainty sets, + True if all scenarios were explicitly accounted for in master + (which occurs if there have been + as many PyROS iterations as there are scenarios in the set) + False otherwise. Attributes ---------- solver_call_results solved_globally worst_case_perf_con + all_discrete_scenarios_exhausted found_violation violating_param_realization scaled_violations @@ -365,11 +372,18 @@ class SeparationLoopResults: time_out """ - def __init__(self, solved_globally, solver_call_results, worst_case_perf_con): + def __init__( + self, + solved_globally, + solver_call_results, + worst_case_perf_con, + all_discrete_scenarios_exhausted=False, + ): """Initialize self (see class docstring).""" self.solver_call_results = solver_call_results self.solved_globally = solved_globally self.worst_case_perf_con = worst_case_perf_con + self.all_discrete_scenarios_exhausted = all_discrete_scenarios_exhausted @property def found_violation(self): @@ -599,6 +613,17 @@ def get_violating_attr(self, attr_name): """ return getattr(self.main_loop_results, attr_name, None) + @property + def all_discrete_scenarios_exhausted(self): + """ + bool : For problems where the uncertainty set is of type + DiscreteScenarioSet, + True if last master problem solved explicitly + accounts for all scenarios in the uncertainty set, + False otherwise. + """ + return self.get_violating_attr("all_discrete_scenarios_exhausted") + @property def worst_case_perf_con(self): """ From 782c4ec093e95b14cdf4ca4aaa2a95ca5a0302b5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 23:45:04 -0700 Subject: [PATCH 0557/1178] Add test guards for pint availability --- pyomo/core/tests/unit/test_numvalue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 2dca2df56a6..442d5bc1a6c 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -18,6 +18,7 @@ import pyomo.common.unittest as unittest from pyomo.common.dependencies import numpy, numpy_available +from pyomo.core.base.units_container import pint_available from pyomo.environ import ( value, @@ -640,8 +641,9 @@ def _tester(expr): _tester('Var() + ref') _tester('v = Var(); v.construct(); v.value = ref') _tester('p = Param(mutable=True); p.construct(); p.value = ref') - _tester('v = Var(units=units.m); v.construct(); v.value = ref') - _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') + if pint_available: + _tester('v = Var(units=units.m); v.construct(); v.value = ref') + _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') if __name__ == "__main__": From e46d2b193a25c321d118ae1474788f5119a155e1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 23:56:19 -0700 Subject: [PATCH 0558/1178] Set maxDiff=None on the base TestCase class --- pyomo/common/tests/test_config.py | 6 ------ pyomo/common/tests/test_log.py | 1 - pyomo/common/tests/test_timing.py | 4 ---- pyomo/common/unittest.py | 4 ++++ pyomo/core/tests/unit/test_block.py | 1 - pyomo/core/tests/unit/test_numeric_expr.py | 1 - pyomo/core/tests/unit/test_reference.py | 2 -- pyomo/core/tests/unit/test_set.py | 1 - pyomo/repn/tests/ampl/test_nlv2.py | 1 - 9 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 12657481764..a47f5e0d8af 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -2098,7 +2098,6 @@ def test_generate_custom_documentation(self): "generate_documentation is deprecated.", LOG, ) - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2113,7 +2112,6 @@ def test_generate_custom_documentation(self): ) ) self.assertEqual(LOG.getvalue(), "") - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2159,7 +2157,6 @@ def test_generate_custom_documentation(self): "generate_documentation is deprecated.", LOG, ) - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2577,7 +2574,6 @@ def test_argparse_help_implicit_disable(self): parser = argparse.ArgumentParser(prog='tester') self.config.initialize_argparse(parser) help = parser.format_help() - self.maxDiff = None self.assertIn( """ -h, --help show this help message and exit @@ -3106,8 +3102,6 @@ def test_declare_from(self): cfg2.declare_from({}) def test_docstring_decorator(self): - self.maxDiff = None - @document_kwargs_from_configdict('CONFIG') class ExampleClass(object): CONFIG = ExampleConfig() diff --git a/pyomo/common/tests/test_log.py b/pyomo/common/tests/test_log.py index 64691c0015a..166e1e44cdb 100644 --- a/pyomo/common/tests/test_log.py +++ b/pyomo/common/tests/test_log.py @@ -511,7 +511,6 @@ def test_verbatim(self): "\n" " quote block\n" ) - self.maxDiff = None self.assertEqual(self.stream.getvalue(), ans) diff --git a/pyomo/common/tests/test_timing.py b/pyomo/common/tests/test_timing.py index 0a4224c5476..48288746882 100644 --- a/pyomo/common/tests/test_timing.py +++ b/pyomo/common/tests/test_timing.py @@ -107,7 +107,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = out.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) finally: @@ -122,7 +121,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = os.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) finally: @@ -135,7 +133,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = os.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) self.assertEqual(buf.getvalue().strip(), "") @@ -172,7 +169,6 @@ def test_report_timing_context_manager(self): xfrm.apply_to(m) self.assertEqual(OUT.getvalue(), "") result = OS.getvalue().strip() - self.maxDiff = None for l, r in zip_longest(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) # Active reporting is False: the previous log should not have changed diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 9a21b35faa8..9ee7731bda4 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -498,6 +498,10 @@ class TestCase(_unittest.TestCase): __doc__ += _unittest.TestCase.__doc__ + # By default, we always want to spend the time to create the full + # diff of the test reault and the baseline + maxDiff = None + def assertStructuredAlmostEqual( self, first, diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 88646643703..71e80d90a73 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2667,7 +2667,6 @@ def test_pprint(self): 5 Declarations: a1_IDX a3_IDX c a b """ - self.maxDiff = None self.assertEqual(ref, buf.getvalue()) @unittest.skipIf(not 'glpk' in solvers, "glpk solver is not available") diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index c073ee0f726..c1066c292d7 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -1424,7 +1424,6 @@ def test_sumOf_nestedTrivialProduct2(self): e1 = m.a * m.p e2 = m.b - m.c e = e2 - e1 - self.maxDiff = None self.assertExpressionsEqual( e, LinearExpression( diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index 287ff204f9e..cfd9b99f945 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -1280,7 +1280,6 @@ def test_contains_with_nonflattened(self): normalize_index.flatten = _old_flatten def test_pprint_nonfinite_sets(self): - self.maxDiff = None m = ConcreteModel() m.v = Var(NonNegativeIntegers, dense=False) m.ref = Reference(m.v) @@ -1322,7 +1321,6 @@ def test_pprint_nonfinite_sets(self): def test_pprint_nonfinite_sets_ctypeNone(self): # test issue #2039 - self.maxDiff = None m = ConcreteModel() m.v = Var(NonNegativeIntegers, dense=False) m.ref = Reference(m.v, ctype=None) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 1ad08ba025c..4bbac6ecaa0 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -6267,7 +6267,6 @@ def test_issue_835(self): @unittest.skipIf(NamedTuple is None, "typing module not available") def test_issue_938(self): - self.maxDiff = None NodeKey = NamedTuple('NodeKey', [('id', int)]) ArcKey = NamedTuple('ArcKey', [('node_from', NodeKey), ('node_to', NodeKey)]) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 215715dba10..86eb43d9a37 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1096,7 +1096,6 @@ def test_log_timing(self): m.c1 = Constraint([1, 2], rule=lambda m, i: sum(m.x.values()) == 1) m.c2 = Constraint(expr=m.p * m.x[1] ** 2 + m.x[2] ** 3 <= 100) - self.maxDiff = None OUT = io.StringIO() with capture_output() as LOG: with report_timing(level=logging.DEBUG): From 66696b33dd17ae61b02b729af24da7ee0cc0164a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 00:40:15 -0700 Subject: [PATCH 0559/1178] Add tests for native type set registration --- pyomo/common/tests/test_numeric_types.py | 219 +++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 pyomo/common/tests/test_numeric_types.py diff --git a/pyomo/common/tests/test_numeric_types.py b/pyomo/common/tests/test_numeric_types.py new file mode 100644 index 00000000000..a6570b7440e --- /dev/null +++ b/pyomo/common/tests/test_numeric_types.py @@ -0,0 +1,219 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.numeric_types as nt +import pyomo.common.unittest as unittest + +from pyomo.common.dependencies import numpy, numpy_available +from pyomo.core.expr import LinearExpression +from pyomo.environ import Var + +_type_sets = ( + 'native_types', + 'native_numeric_types', + 'native_logical_types', + 'native_integer_types', + 'native_complex_types', +) + + +class TestNativeTypes(unittest.TestCase): + def setUp(self): + bool(numpy_available) + for s in _type_sets: + setattr(self, s, set(getattr(nt, s))) + getattr(nt, s).clear() + + def tearDown(self): + for s in _type_sets: + getattr(nt, s).clear() + getattr(nt, s).update(getattr(nt, s)) + + def test_check_if_native_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertTrue(nt.check_if_native_type("a")) + self.assertIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(1)) + self.assertIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertIn(int, nt.native_numeric_types) + self.assertIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(1.5)) + self.assertIn(float, nt.native_types) + self.assertNotIn(float, nt.native_logical_types) + self.assertIn(float, nt.native_numeric_types) + self.assertNotIn(float, nt.native_integer_types) + self.assertNotIn(float, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(True)) + self.assertIn(bool, nt.native_types) + self.assertIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertFalse(nt.check_if_native_type(slice(None, None, None))) + self.assertNotIn(slice, nt.native_types) + self.assertNotIn(slice, nt.native_logical_types) + self.assertNotIn(slice, nt.native_numeric_types) + self.assertNotIn(slice, nt.native_integer_types) + self.assertNotIn(slice, nt.native_complex_types) + + def test_check_if_logical_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertFalse(nt.check_if_logical_type("a")) + self.assertNotIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertFalse(nt.check_if_logical_type("a")) + + self.assertTrue(nt.check_if_logical_type(True)) + self.assertIn(bool, nt.native_types) + self.assertIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertTrue(nt.check_if_logical_type(True)) + + self.assertFalse(nt.check_if_logical_type(1)) + self.assertNotIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertNotIn(int, nt.native_numeric_types) + self.assertNotIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + if numpy_available: + self.assertTrue(nt.check_if_logical_type(numpy.bool_(1))) + self.assertIn(numpy.bool_, nt.native_types) + self.assertIn(numpy.bool_, nt.native_logical_types) + self.assertNotIn(numpy.bool_, nt.native_numeric_types) + self.assertNotIn(numpy.bool_, nt.native_integer_types) + self.assertNotIn(numpy.bool_, nt.native_complex_types) + + def test_check_if_numeric_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertFalse(nt.check_if_numeric_type("a")) + self.assertFalse(nt.check_if_numeric_type("a")) + self.assertNotIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(True)) + self.assertFalse(nt.check_if_numeric_type(True)) + self.assertNotIn(bool, nt.native_types) + self.assertNotIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(1)) + self.assertTrue(nt.check_if_numeric_type(1)) + self.assertIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertIn(int, nt.native_numeric_types) + self.assertIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(1.5)) + self.assertTrue(nt.check_if_numeric_type(1.5)) + self.assertIn(float, nt.native_types) + self.assertNotIn(float, nt.native_logical_types) + self.assertIn(float, nt.native_numeric_types) + self.assertNotIn(float, nt.native_integer_types) + self.assertNotIn(float, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(1j)) + self.assertIn(complex, nt.native_types) + self.assertNotIn(complex, nt.native_logical_types) + self.assertNotIn(complex, nt.native_numeric_types) + self.assertNotIn(complex, nt.native_integer_types) + self.assertIn(complex, nt.native_complex_types) + + v = Var() + v.construct() + self.assertFalse(nt.check_if_numeric_type(v)) + self.assertNotIn(type(v), nt.native_types) + self.assertNotIn(type(v), nt.native_logical_types) + self.assertNotIn(type(v), nt.native_numeric_types) + self.assertNotIn(type(v), nt.native_integer_types) + self.assertNotIn(type(v), nt.native_complex_types) + + e = LinearExpression([1]) + self.assertFalse(nt.check_if_numeric_type(e)) + self.assertNotIn(type(e), nt.native_types) + self.assertNotIn(type(e), nt.native_logical_types) + self.assertNotIn(type(e), nt.native_numeric_types) + self.assertNotIn(type(e), nt.native_integer_types) + self.assertNotIn(type(e), nt.native_complex_types) + + if numpy_available: + self.assertFalse(nt.check_if_numeric_type(numpy.bool_(1))) + self.assertNotIn(numpy.bool_, nt.native_types) + self.assertNotIn(numpy.bool_, nt.native_logical_types) + self.assertNotIn(numpy.bool_, nt.native_numeric_types) + self.assertNotIn(numpy.bool_, nt.native_integer_types) + self.assertNotIn(numpy.bool_, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(numpy.array([1]))) + self.assertNotIn(numpy.ndarray, nt.native_types) + self.assertNotIn(numpy.ndarray, nt.native_logical_types) + self.assertNotIn(numpy.ndarray, nt.native_numeric_types) + self.assertNotIn(numpy.ndarray, nt.native_integer_types) + self.assertNotIn(numpy.ndarray, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(numpy.float64(1))) + self.assertIn(numpy.float64, nt.native_types) + self.assertNotIn(numpy.float64, nt.native_logical_types) + self.assertIn(numpy.float64, nt.native_numeric_types) + self.assertNotIn(numpy.float64, nt.native_integer_types) + self.assertNotIn(numpy.float64, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(numpy.int64(1))) + self.assertIn(numpy.int64, nt.native_types) + self.assertNotIn(numpy.int64, nt.native_logical_types) + self.assertIn(numpy.int64, nt.native_numeric_types) + self.assertIn(numpy.int64, nt.native_integer_types) + self.assertNotIn(numpy.int64, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(numpy.complex128(1))) + self.assertIn(numpy.complex128, nt.native_types) + self.assertNotIn(numpy.complex128, nt.native_logical_types) + self.assertNotIn(numpy.complex128, nt.native_numeric_types) + self.assertNotIn(numpy.complex128, nt.native_integer_types) + self.assertIn(numpy.complex128, nt.native_complex_types) From 5b6cf69c862e8a97605eb272561e60b73ae640f1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 00:43:22 -0700 Subject: [PATCH 0560/1178] NFC: apply black --- pyomo/core/tests/unit/test_numvalue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 442d5bc1a6c..1cccd3863ea 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -643,7 +643,9 @@ def _tester(expr): _tester('p = Param(mutable=True); p.construct(); p.value = ref') if pint_available: _tester('v = Var(units=units.m); v.construct(); v.value = ref') - _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') + _tester( + 'p = Param(mutable=True, units=units.m); p.construct(); p.value = ref' + ) if __name__ == "__main__": From 1a347bf7fea5430cd1041e408f4b66cdcc874e68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 00:52:46 -0700 Subject: [PATCH 0561/1178] Fix typo restoring state after test --- pyomo/common/tests/test_numeric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/tests/test_numeric_types.py b/pyomo/common/tests/test_numeric_types.py index a6570b7440e..b7ffb5fb255 100644 --- a/pyomo/common/tests/test_numeric_types.py +++ b/pyomo/common/tests/test_numeric_types.py @@ -35,7 +35,7 @@ def setUp(self): def tearDown(self): for s in _type_sets: getattr(nt, s).clear() - getattr(nt, s).update(getattr(nt, s)) + getattr(nt, s).update(getattr(self, s)) def test_check_if_native_type(self): self.assertEqual(nt.native_types, set()) From caa688ed390e609b78ff3af305f87223dd3a69e6 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:12:00 +0100 Subject: [PATCH 0562/1178] Initial point calculation consistent with ALE syntax --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index dcb8040eabe..530521f6b83 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -146,7 +146,7 @@ def get_variables(self): ] def get_initial_point(self): - return [var.init if not var.init is None else var.lb for var in self._var_list] + return [var.init if not var.init is None else (var.lb + var.ub)/2.0 for var in self._var_list] def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) From 365370a4220ff2fe0df818878f89b5f6fe36d7a3 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:24:17 +0100 Subject: [PATCH 0563/1178] Added warning for missing variable bounds --- pyomo/contrib/appsi/solvers/maingo.py | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 530521f6b83..52b12d434ef 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -436,10 +436,28 @@ def solve(self, model, timer: HierarchicalTimer = None): def _process_domain_and_bounds(self, var): _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] lb, ub, step = _domain_interval - if lb is None: - lb = -1e10 - if ub is None: - ub = 1e10 + + if _fixed: + lb = _value + ub = _value + else: + if lb is None and _lb is None: + logger.warning("No lower bound for variable " + var.getname() + " set. Using -1e10 instead. Please consider setting a valid lower bound.") + if ub is None and _ub is None: + logger.warning("No upper bound for variable " + var.getname() + " set. Using +1e10 instead. Please consider setting a valid upper bound.") + + if _lb is None: + _lb = -1e10 + if _ub is None: + _ub = 1e10 + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + + lb = max(value(_lb), lb) + ub = min(value(_ub), ub) + if step == 0: vtype = maingopy.VT_CONTINUOUS elif step == 1: @@ -451,14 +469,7 @@ def _process_domain_and_bounds(self, var): raise ValueError( f"Unrecognized domain step: {step} (should be either 0 or 1)" ) - if _fixed: - lb = _value - ub = _value - else: - if _lb is not None: - lb = max(value(_lb), lb) - if _ub is not None: - ub = min(value(_ub), ub) + return lb, ub, vtype From c440037d95146caaa896404235ebd2b60f8d8926 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:32:55 +0100 Subject: [PATCH 0564/1178] Added: NotImplementedError for SOS constraints --- pyomo/contrib/appsi/solvers/maingo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 52b12d434ef..99cff4b7aa9 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -525,12 +525,16 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): self._cons = cons def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError("MAiNGO does not currently support SOS constraints.") pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): pass def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError("MAiNGO does not currently support SOS constraints.") pass def _remove_variables(self, variables: List[_GeneralVarData]): From 52a4cd9e59baf27bdab4df32b3a850d511ce894b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:39:28 +0100 Subject: [PATCH 0565/1178] Changed: Formulation of asinh, acosh, atanh --- pyomo/contrib/appsi/solvers/maingo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 99cff4b7aa9..099865f5a84 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -192,15 +192,15 @@ def maingo_log10(cls, x): @classmethod def maingo_asinh(cls, x): - return maingopy.inv(maingopy.sinh(x)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) + 1)) @classmethod def maingo_acosh(cls, x): - return maingopy.inv(maingopy.cosh(x)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) - 1)) @classmethod def maingo_atanh(cls, x): - return maingopy.inv(maingopy.tanh(x)) + return 0.5 * maingopy.log(x+1) - 0.5 * maingopy.log(1-x) def visit(self, node, values): """Visit nodes that have been expanded""" From 44311203d1b426a0894e8f9c7efbdbfa3c0eec85 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:49:15 +0100 Subject: [PATCH 0566/1178] Added: Warning for non-global solutions --- pyomo/contrib/appsi/solvers/maingo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 099865f5a84..7a153f938b7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -570,8 +570,10 @@ def _postsolve(self, timer: HierarchicalTimer): results.wallclock_time = mprob.get_wallclock_solution_time() results.cpu_time = mprob.get_cpu_solution_time() - if status == maingopy.GLOBALLY_OPTIMAL: + if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal + if status == maingopy.FEASIBLE_POINT: + logger.warning("MAiNGO did only find a feasible solution but did not prove its global optimality.") elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible else: From ff2c9bd86eefe7b2693f60a165383b525a764984 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:57:01 +0100 Subject: [PATCH 0567/1178] Changed: absolute to relative MIP gap --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 7a153f938b7..cf09b42fbda 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -407,7 +407,7 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) if config.mip_gap is not None: - self._mymaingo.set_option("epsilonA", config.mip_gap) + self._mymaingo.set_option("epsilonR", config.mip_gap) for key, option in options.items(): self._mymaingo.set_option(key, option) From b833ac729aa7741331811c133d54d809f27e17be Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:11:48 +0100 Subject: [PATCH 0568/1178] Added: Maingopy version --- pyomo/contrib/appsi/solvers/maingo.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index cf09b42fbda..05c5f50a295 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -354,7 +354,15 @@ def available(self): return self._available def version(self): - pass + # Check if Python >= 3.8 + if sys.version_info.major >= 3 and sys.version_info.minor >= 8: + from importlib.metadata import version + version = version('maingopy') + else: + import pkg_resources + version = pkg_resources.get_distribution('maingopy').version + + return tuple(int(k) for k in version.split('.')) @property def config(self) -> MAiNGOConfig: From c937b63e2e6e699efe24ef0e46cd36fd56da8134 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:21:12 +0100 Subject: [PATCH 0569/1178] Black Formatting --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/maingo.py | 42 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c9e0a2a003d..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO \ No newline at end of file +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 05c5f50a295..d98b33af998 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -146,7 +146,10 @@ def get_variables(self): ] def get_initial_point(self): - return [var.init if not var.init is None else (var.lb + var.ub)/2.0 for var in self._var_list] + return [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) @@ -192,15 +195,15 @@ def maingo_log10(cls, x): @classmethod def maingo_asinh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) + 1)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) @classmethod def maingo_acosh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) - 1)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) @classmethod def maingo_atanh(cls, x): - return 0.5 * maingopy.log(x+1) - 0.5 * maingopy.log(1-x) + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) def visit(self, node, values): """Visit nodes that have been expanded""" @@ -357,11 +360,13 @@ def version(self): # Check if Python >= 3.8 if sys.version_info.major >= 3 and sys.version_info.minor >= 8: from importlib.metadata import version + version = version('maingopy') else: import pkg_resources + version = pkg_resources.get_distribution('maingopy').version - + return tuple(int(k) for k in version.split('.')) @property @@ -450,10 +455,18 @@ def _process_domain_and_bounds(self, var): ub = _value else: if lb is None and _lb is None: - logger.warning("No lower bound for variable " + var.getname() + " set. Using -1e10 instead. Please consider setting a valid lower bound.") + logger.warning( + "No lower bound for variable " + + var.getname() + + " set. Using -1e10 instead. Please consider setting a valid lower bound." + ) if ub is None and _ub is None: - logger.warning("No upper bound for variable " + var.getname() + " set. Using +1e10 instead. Please consider setting a valid upper bound.") - + logger.warning( + "No upper bound for variable " + + var.getname() + + " set. Using +1e10 instead. Please consider setting a valid upper bound." + ) + if _lb is None: _lb = -1e10 if _ub is None: @@ -478,7 +491,6 @@ def _process_domain_and_bounds(self, var): f"Unrecognized domain step: {step} (should be either 0 or 1)" ) - return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): @@ -534,7 +546,9 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: - raise NotImplementedError("MAiNGO does not currently support SOS constraints.") + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): @@ -542,7 +556,9 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: - raise NotImplementedError("MAiNGO does not currently support SOS constraints.") + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) pass def _remove_variables(self, variables: List[_GeneralVarData]): @@ -581,7 +597,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal if status == maingopy.FEASIBLE_POINT: - logger.warning("MAiNGO did only find a feasible solution but did not prove its global optimality.") + logger.warning( + "MAiNGO did only find a feasible solution but did not prove its global optimality." + ) elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible else: From ccb7723466f0bb06eb3abf552c44c084932704b3 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:44:02 +0100 Subject: [PATCH 0570/1178] Fixed: Black Formatting --- pyomo/contrib/appsi/solvers/maingo.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d98b33af998..614e12d227b 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -318,12 +318,14 @@ def _monomial_to_maingo(self, node): def _linear_to_maingo(self, node): values = [ - self._monomial_to_maingo(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + ( + self._monomial_to_maingo(arg) + if ( + arg.__class__ is EXPR.MonomialTermExpression + and not arg.arg(1).is_fixed() + ) + else value(arg) ) - else value(arg) for arg in node.args ] return sum(values) From e28e14db71b7ee09a54af2882d80837289f5c448 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 14:33:06 +0100 Subject: [PATCH 0571/1178] Added: pip install maingopy to test_branches.yml --- .github/workflows/test_branches.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 77f47b505ff..1441cd53623 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -268,6 +268,8 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip maingopy \ + || echo "WARNING: MAiNGO is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else From c52bbc7f3c565d5d50ff98187efdb8d25de32e4b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 14:35:41 +0100 Subject: [PATCH 0572/1178] Added: pip install maingopy to test_pr_and_main.yml --- .github/workflows/test_pr_and_main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 87d6aa4d7a8..0214442d4e5 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -298,6 +298,8 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip maingopy \ + || echo "WARNING: MAiNGO is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else From 84ca52464286faeb95fd5edda052b4d3459f021c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 08:35:37 -0700 Subject: [PATCH 0573/1178] NFC: fix comment typo --- pyomo/common/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index edf32baa6d6..895759a8a2c 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -513,7 +513,7 @@ def invalidate_caches(self): _DeferredImportCallbackFinder = DeferredImportCallbackFinder() # Insert the DeferredImportCallbackFinder at the beginning of the -# sys.meta_path to that it is found before the standard finders (so that +# sys.meta_path so that it is found before the standard finders (so that # we can correctly inject the resolution of the DeferredImportIndicators # -- which triggers the needed callbacks) sys.meta_path.insert(0, _DeferredImportCallbackFinder) From de86a6aa93374baf6e0f6147a13421c9ef45c49e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 09:03:49 -0700 Subject: [PATCH 0574/1178] check_if_logical_type(): expand Boolean tests, relax cast-from-int requirement --- pyomo/common/numeric_types.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 616d4c4bae4..9d4adc12e22 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -229,13 +229,32 @@ def check_if_logical_type(obj): return obj_class in native_logical_types try: + # It is not an error if you can't initialize the type from an + # int, but if you can, it should map !0 to True + if obj_class(1) != obj_class(2): + return False + except: + pass + + try: + # Native logical types *must* be hashable + hash(obj) + # Native logical types must honor standard Boolean operators if all( ( - obj_class(1) == obj_class(2), obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(False) == obj_class(False), obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(True) ^ obj_class(False) == obj_class(True), + obj_class(True) ^ obj_class(True) == obj_class(False), + obj_class(False) | obj_class(False) == obj_class(False), obj_class(False) | obj_class(True) == obj_class(True), + obj_class(True) | obj_class(False) == obj_class(True), + obj_class(True) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(False) == obj_class(False), obj_class(False) & obj_class(True) == obj_class(False), + obj_class(True) & obj_class(False) == obj_class(False), + obj_class(True) & obj_class(True) == obj_class(True), ) ): RegisterLogicalType(obj_class) From e617a6e773c16ff840a4f22cd9751eaadf1ed132 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 09:04:16 -0700 Subject: [PATCH 0575/1178] NFC: update comments/docstrings --- pyomo/common/numeric_types.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 9d4adc12e22..a1fe1e7514e 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -217,10 +217,10 @@ def check_if_native_type(obj): def check_if_logical_type(obj): """Test if the argument behaves like a logical type. - We check for "numeric types" by checking if we can add zero to it - without changing the object's type, and that the object compares to - 0 in a meaningful way. If that works, then we register the type in - :py:attr:`native_numeric_types`. + We check for "logical types" by checking if the type returns sane + results for Boolean operators (``^``, ``|``, ``&``) and if it maps + ``1`` and ``2`` both to the same equivalent instance. If that + works, then we register the type in :py:attr:`native_logical_types`. """ obj_class = obj.__class__ @@ -304,9 +304,8 @@ def check_if_numeric_type(obj): except: pass # - # ensure that the object is comparable to 0 in a meaningful way - # (among other things, this prevents numpy.ndarray objects from - # being added to native_numeric_types) + # Ensure that the object is comparable to 0 in a meaningful way + # try: if not ((obj < 0) ^ (obj >= 0)): return False From a931ace7b198e4db00970ab0c009d3c4e5805959 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:33:43 -0800 Subject: [PATCH 0576/1178] minor updates to datarec example --- .../reactor_design/datarec_example.py | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 00287730c63..45f826f880d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -20,15 +20,6 @@ np.random.seed(1234) -def reactor_design_model_for_datarec(): - - # Unfix inlet concentration for data rec - model = reactor_design_model() - model.caf.fixed = False - - return model - - class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): @@ -37,7 +28,10 @@ def __init__(self, data, data_std, experiment_number): self.data_std = data_std def create_model(self): - self.model = m = reactor_design_model_for_datarec() + + self.model = m = reactor_design_model() + m.caf.fixed = False + return m def label_model(self): @@ -124,20 +118,15 @@ def main(): exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) # Define sum of squared error objective function for data rec - def SSE(model): + def SSE_with_std(model): expr = sum( ((y - yhat) / model.experiment_outputs_std[y]) ** 2 for y, yhat in model.experiment_outputs.items() ) return expr - # View one model & SSE - # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) - # print(SSE(exp0_model)) - ### Data reconciliation - pest = parmest.Estimator(exp_list, obj_function=SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE_with_std) obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) @@ -157,7 +146,7 @@ def SSE(model): for i in range(data_rec.shape[0]): exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) - pest = parmest.Estimator(exp_list, obj_function=SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE_with_std) obj, theta = pest.theta_est() print(obj) print(theta) From f5350a4f45982fb7661dd468d94df5bfc8880164 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:34:28 -0800 Subject: [PATCH 0577/1178] Added API docs and made deprecated class private --- pyomo/contrib/parmest/parmest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ffe9afc059e..8f98ab5cd5a 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -276,6 +276,9 @@ def _experiment_instance_creation_callback( def SSE(model): + """ + Sum of squared error between `experiment_output` model and data values + """ expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr @@ -330,7 +333,7 @@ def __init__(self, *args, **kwargs): + 'data, theta_names), please use experiment lists instead.', version=DEPRECATION_VERSION, ) - self.pest_deprecated = DeprecatedEstimator(*args, **kwargs) + self.pest_deprecated = _DeprecatedEstimator(*args, **kwargs) return # check that we have a (non-empty) list of experiments @@ -1477,7 +1480,7 @@ def __call__(self, model): return self._ssc_function(model, self._data) -class DeprecatedEstimator(object): +class _DeprecatedEstimator(object): """ Parameter estimation class From 1761879c34b93c8c6d573cdbdbd1527555d14817 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:36:19 -0800 Subject: [PATCH 0578/1178] renamed class in datarec example --- .../parmest/examples/reactor_design/datarec_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 45f826f880d..e05b69aa4cc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -20,7 +20,7 @@ np.random.seed(1234) -class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): +class ReactorDesignExperimentDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): @@ -115,7 +115,7 @@ def main(): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) + exp_list.append(ReactorDesignExperimentDataRec(data, data_std, i)) # Define sum of squared error objective function for data rec def SSE_with_std(model): From 97469b24ed63d4b635952b36008656162486f11e Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:43:25 -0800 Subject: [PATCH 0579/1178] parmest doc updates --- .../contributed_packages/parmest/datarec.rst | 35 +++--- .../contributed_packages/parmest/driver.rst | 105 ++++++++---------- .../contributed_packages/parmest/examples.rst | 2 +- .../parmest/scencreate.rst | 2 +- 4 files changed, 60 insertions(+), 84 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index 6e9be904286..2260450192c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -3,35 +3,27 @@ Data Reconciliation ==================== -The method :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` -can optionally return model values. This feature can be used to return -reconciled data using a user specified objective. In this case, the list -of variable names the user wants to estimate (theta_names) is set to an -empty list and the objective function is defined to minimize +The optional argument ``return_values`` in :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` +can be used for data reconciliation or to return model values based on the specified objective. + +For data reconciliation, the ``m.unknown_parameters`` is empty +and the objective function is defined to minimize measurement to model error. Note that the model used for data reconciliation may differ from the model used for parameter estimation. -The following example illustrates the use of parmest for data -reconciliation. The functions +The functions :class:`~pyomo.contrib.parmest.graphics.grouped_boxplot` or :class:`~pyomo.contrib.parmest.graphics.grouped_violinplot` can be used to visually compare the original and reconciled data. -Here's a stylized code snippet showing how box plots might be created: - -.. doctest:: - :skipif: True - - >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(model_function, data, [], objective_function) - >>> obj, theta, data_rec = pest.theta_est(return_values=['A', 'B']) - >>> parmest.graphics.grouped_boxplot(data, data_rec) +The following example from the reactor design subdirectory returns reconciled values for experiment outputs +(`ca`, `cb`, `cc`, and `cd`) and then uses those values in +parameter estimation (`k1`, `k2`, and `k3`). -Returned Values -^^^^^^^^^^^^^^^ - -Here's a full program that can be run to see returned values (in this case it -is the response function that is defined in the model file): +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/datarec_example.py + :language: python + +The following example returns model values from a Pyomo Expression. .. doctest:: :skipif: not ipopt_available or not parmest_available @@ -60,4 +52,3 @@ is the response function that is defined in the model file): >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) >>> #print(var_values) - diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 45533e9520c..695cab36e93 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -4,7 +4,7 @@ Parameter Estimation ================================== Parameter Estimation using parmest requires a Pyomo model, experimental -data which defines multiple scenarios, and a list of parameter names +data which defines multiple scenarios, and parameters (thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) mpi-sppy [mpisppy]_ to solve a two-stage stochastic programming problem, where the experimental data is @@ -36,8 +36,8 @@ which includes the following methods: ~pyomo.contrib.parmest.parmest.Estimator.likelihood_ratio_test ~pyomo.contrib.parmest.parmest.Estimator.leaveNout_bootstrap_test -Additional functions are available in parmest to group data, plot -results, and fit distributions to theta values. +Additional functions are available in parmest to plot +results and fit distributions to theta values. .. autosummary:: :nosignatures: @@ -92,65 +92,43 @@ Optionally, solver options can be supplied, e.g., >>> solver_options = {"max_iter": 6000} >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) - - - -Model function --------------- - -The first argument is a function which uses data for a single scenario -to return a populated and initialized Pyomo model for that scenario. - -Parameters that the user would like to estimate can be defined as -**mutable parameters (Pyomo `Param`) or variables (Pyomo `Var`)**. -Within parmest, any parameters that are to be estimated are converted to unfixed variables. -Variables that are to be estimated are also unfixed. - -The model does not have to be specifically written as a -two-stage stochastic programming problem for parmest. -That is, parmest can modify the -objective, see :ref:`ObjFunction` below. - -Data ----- - -The second argument is the data which will be used to populate the Pyomo -model. Supported data formats include: - -* **Pandas Dataframe** where each row is a separate scenario and column - names refer to observed quantities. Pandas DataFrames are easily - stored and read in from csv, excel, or databases, or created directly - in Python. -* **List of Pandas Dataframe** where each entry in the list is a separate scenario. - Dataframes store observed quantities, referenced by index and column. -* **List of dictionaries** where each entry in the list is a separate - scenario and the keys (or nested keys) refer to observed quantities. - Dictionaries are often preferred over DataFrames when using static and - time series data. Dictionaries are easily stored and read in from - json or yaml files, or created directly in Python. -* **List of json file names** where each entry in the list contains a - json file name for a separate scenario. This format is recommended - when using large datasets in parallel computing. - -The data must be compatible with the model function that returns a -populated and initialized Pyomo model for a single scenario. Data can -include multiple entries per variable (time series and/or duplicate -sensors). This information can be included in custom objective -functions, see :ref:`ObjFunction` below. - -Theta names ------------ - -The third argument is a list of parameters or variable names that the user wants to -estimate. The list contains strings with `Param` and/or `Var` names from the Pyomo -model. + + +List of experiment objects +-------------------------- + +The first argument is a list of experiment objects which is used to +create one labeled model for each expeirment. +The template :class:`~pyomo.contrib.parmest.experiment.Experiment` +can be used to generate a list of experiment objects. + +A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix`): + +* ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) + and their associated data values (float, int). +* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Parm` or `Var`) + to estimate along with their component unique identifier (Pyomo `ComponentUID`). + Within parmest, any parameters that are to be estimated are converted to unfixed variables. + Variables that are to be estimated are also unfixed. + +The experiment class has one required method: + +* :class:`~pyomo.contrib.parmest.experiment.Experiment.get_labeled_model` which returns the labeled Pyomo model. + Note that the model does not have to be specifically written as a + two-stage stochastic programming problem for parmest. + That is, parmest can modify the + objective, see :ref:`ObjFunction` below. + +Parmest comes with several :ref:`examplesection` that illustrates how to set up the list of experiment objects. +The examples commonly include additional :class:`~pyomo.contrib.parmest.experiment.Experiment` class methods to +create the model, finalize the model, and label the model. The user can customize methods to suit their needs. .. _ObjFunction: Objective function ------------------ -The fourth argument is an optional argument which defines the +The second argument is an optional argument which defines the optimization objective function to use in parameter estimation. If no objective function is specified, the Pyomo model is used "as is" and @@ -161,20 +139,27 @@ stochastic programming problem. If the Pyomo model is not written as a two-stage stochastic programming problem in this format, and/or if the user wants to use an objective that is different than the original model, a custom objective function can be -defined for parameter estimation. The objective function arguments -include `model` and `data` and the objective function returns a Pyomo +defined for parameter estimation. The objective function has a single argument, +which is the model from a single experiment. +The objective function returns a Pyomo expression which is used to define "SecondStageCost". The objective function can be used to customize data points and weights that are used in parameter estimation. +Parmest includes one built in objective function to compute the sum of squared errors ("SSE") between the +``m.experiment_outputs`` model values and data values. + Suggested initialization procedure for parameter estimation problems -------------------------------------------------------------------- To check the quality of initial guess values provided for the fitted parameters, we suggest solving a square instance of the problem prior to solving the parameter estimation problem using the following steps: -1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. +1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter +estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. -2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) +2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional +argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted +parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) 3. Solve parameter estimation problem by calling :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` diff --git a/doc/OnlineDocs/contributed_packages/parmest/examples.rst b/doc/OnlineDocs/contributed_packages/parmest/examples.rst index 793ff3d0c8d..a59d79dfa2b 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/examples.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/examples.rst @@ -20,7 +20,7 @@ Additional use cases include: * Parameter estimation using mpi4py, the example saves results to a file for later analysis/graphics (semibatch example) -The description below uses the reactor design example. The file +The example below uses the reactor design example. The file **reactor_design.py** includes a function which returns an populated instance of the Pyomo model. Note that the model is defined to maximize `cb` and that `k1`, `k2`, and `k3` are fixed. The _main_ program is diff --git a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst b/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst index 66d41d4c606..b63ac5893c2 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst @@ -18,5 +18,5 @@ scenarios to the screen, accessing them via the ``ScensItator`` a ``print`` :language: python .. note:: - This example may produce an error message your version of Ipopt is not based + This example may produce an error message if your version of Ipopt is not based on a good linear solver. From ba840aabf0c1281b68524cf11ed67ddbc940b89e Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:55:17 -0800 Subject: [PATCH 0580/1178] added header and updated API docs for Experiment --- pyomo/contrib/parmest/experiment.py | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py index e16ad304e42..69474a32bb0 100644 --- a/pyomo/contrib/parmest/experiment.py +++ b/pyomo/contrib/parmest/experiment.py @@ -1,14 +1,28 @@ -# The experiment class is a template for making experiment lists -# to pass to parmest. An experiment is a pyomo model "m" which has -# additional suffixes: -# m.experiment_outputs -- which variables are experiment outputs -# m.unknown_parameters -- which variables are parameters to estimate -# The experiment class has only one required method: -# get_labeled_model() -# which returns the labeled pyomo model. +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ class Experiment: + """ + The experiment class is a template for making experiment lists + to pass to parmest. + + An experiment is a Pyomo model "m" which is labeled + with additional suffixes: + * m.experiment_outputs which defines experiment outputs + * m.unknown_parameters which defines parameters to estimate + + The experiment class has one required method: + * get_labeled_model() which returns the labeled Pyomo model + """ def __init__(self, model=None): self.model = model From be878d18b608b3bd1e0b67c70caefc5d83b86029 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:55:32 -0800 Subject: [PATCH 0581/1178] made ScenarioCreatorDeprecated class private --- pyomo/contrib/parmest/scenariocreator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 434e15e7f31..f2798ad2e94 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -137,7 +137,7 @@ def __init__(self, pest, solvername): + "creator, please recreate object using experiment lists.", version=DEPRECATION_VERSION, ) - self.scen_deprecated = ScenarioCreatorDeprecated( + self.scen_deprecated = _ScenarioCreatorDeprecated( pest.pest_deprecated, solvername ) else: @@ -201,7 +201,7 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): ################################ -class ScenarioCreatorDeprecated(object): +class _ScenarioCreatorDeprecated(object): """Create scenarios from parmest. Args: From 46bfee38dbdcc11c76e8279207b7ac2d0dfeafb7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 11:02:12 -0700 Subject: [PATCH 0582/1178] NFC: removing a comment that is no longer relevant --- pyomo/common/dependencies.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 895759a8a2c..472b0011edb 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -1034,12 +1034,6 @@ def _pyutilib_importer(): return importlib.import_module('pyutilib') -# -# Note: because we will be calling -# declare_deferred_modules_as_importable, it is important that the -# following declarations explicitly defer_import (even if the target -# module has already been imported) -# with declare_modules_as_importable(globals()): # Standard libraries that are slower to import and not strictly required # on all platforms / situations. From 9cb26c7629c49b055ec699e505e4c5ab32d07d62 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 28 Feb 2024 11:16:41 -0700 Subject: [PATCH 0583/1178] NFC: Fix year in copyright assertion comment Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/contrib/simplification/__init__.py | 2 +- pyomo/contrib/simplification/build.py | 2 +- pyomo/contrib/simplification/ginac_interface.cpp | 2 +- pyomo/contrib/simplification/simplify.py | 2 +- pyomo/contrib/simplification/tests/__init__.py | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index b4fa68eb386..c6111ddcb89 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d9d1e701290..2c7b1830ff6 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 489f281bc2c..1060f87161c 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,7 +1,7 @@ // ___________________________________________________________________________ // // Pyomo: Python Optimization Modeling Objects -// Copyright (c) 2008-2022 +// Copyright (c) 2008-2024 // National Technology and Engineering Solutions of Sandia, LLC // Under the terms of Contract DE-NA0003525 with National Technology and // Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index b8cc4995f91..00c5dde348e 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 95402f98318..1a5ae1e0036 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 8b2568804f83f89d24eb53330ffae61dbc95b3c7 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 10:48:49 -0800 Subject: [PATCH 0584/1178] formatting updates --- pyomo/contrib/parmest/experiment.py | 9 +++++---- pyomo/contrib/parmest/parmest.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py index 69474a32bb0..4f797d6c89c 100644 --- a/pyomo/contrib/parmest/experiment.py +++ b/pyomo/contrib/parmest/experiment.py @@ -13,16 +13,17 @@ class Experiment: """ The experiment class is a template for making experiment lists - to pass to parmest. - - An experiment is a Pyomo model "m" which is labeled + to pass to parmest. + + An experiment is a Pyomo model "m" which is labeled with additional suffixes: * m.experiment_outputs which defines experiment outputs * m.unknown_parameters which defines parameters to estimate - + The experiment class has one required method: * get_labeled_model() which returns the labeled Pyomo model """ + def __init__(self, model=None): self.model = model diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 8f98ab5cd5a..aecc9d5ebc2 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -408,7 +408,7 @@ def _expand_indexed_unknowns(self, model_temp): Expand indexed variables to get full list of thetas """ model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] - + # check for indexed theta items indexed_theta_list = [] for theta_i in model_theta_list: @@ -419,11 +419,11 @@ def _expand_indexed_unknowns(self, model_temp): indexed_theta_list.append(theta_i + '[' + str(ind) + ']') else: indexed_theta_list.append(theta_i) - + # if we found indexed thetas, use expanded list if len(indexed_theta_list) > len(model_theta_list): model_theta_list = indexed_theta_list - + return model_theta_list def _create_parmest_model(self, experiment_number): From c5bdb0ba866bb0be1dd956fca35332ef84b88d5b Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 12:01:02 -0800 Subject: [PATCH 0585/1178] fixed typo --- doc/OnlineDocs/contributed_packages/parmest/driver.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 695cab36e93..5881d2748f9 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -106,7 +106,7 @@ A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix * ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) and their associated data values (float, int). -* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Parm` or `Var`) +* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Param` or `Var`) to estimate along with their component unique identifier (Pyomo `ComponentUID`). Within parmest, any parameters that are to be estimated are converted to unfixed variables. Variables that are to be estimated are also unfixed. From 5b5f0046ab59accedee601deb51cfe14939298ec Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 28 Feb 2024 15:24:45 -0500 Subject: [PATCH 0586/1178] Fix indentation typo --- pyomo/contrib/pyros/solve_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index c31eb8e5d3f..73eee5202aa 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -347,7 +347,7 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or Constraint + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. From 20a63602692ee8c9e8ee2bfa9efa95af392f5a6e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 28 Feb 2024 15:52:14 -0500 Subject: [PATCH 0587/1178] Update `positive_int_or_minus_1` tests --- pyomo/contrib/pyros/tests/test_config.py | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 3555391fd95..0f52d04135d 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -558,21 +558,28 @@ def test_positive_int_or_minus_one(self): Test positive int or -1 validator works as expected. """ standardizer_func = positive_int_or_minus_one - self.assertIs( - standardizer_func(1.0), + ans = standardizer_func(1.0) + self.assertEqual( + ans, 1, - msg=( - f"{positive_int_or_minus_one.__name__} " - "does not standardize as expected." - ), + msg=f"{positive_int_or_minus_one.__name__} output value not as expected.", + ) + self.assertIs( + type(ans), + int, + msg=f"{positive_int_or_minus_one.__name__} output type not as expected.", ) + + ans = standardizer_func(-1.0) self.assertEqual( - standardizer_func(-1.00), + ans, -1, - msg=( - f"{positive_int_or_minus_one.__name__} " - "does not standardize as expected." - ), + msg=f"{positive_int_or_minus_one.__name__} output value not as expected.", + ) + self.assertIs( + type(ans), + int, + msg=f"{positive_int_or_minus_one.__name__} output type not as expected.", ) exc_str = r"Expected positive int or -1, but received value.*" From 4e5bbbf2f073911ed89d39002ea96b652e807e16 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 22:58:51 -0700 Subject: [PATCH 0588/1178] NFC: fix copyright header --- pyomo/contrib/latex_printer/latex_printer.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 110df7cd5ca..a986f5d6b81 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -9,17 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import math import copy import re From 0e673b663ad339e00ca93a65cf414bd0f79ca012 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:01:38 -0700 Subject: [PATCH 0589/1178] performance: avoid duplication, linear searches --- pyomo/contrib/latex_printer/latex_printer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index a986f5d6b81..41cff29ad80 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -275,11 +275,11 @@ def handle_functionID_node(visitor, node, *args): def handle_indexTemplate_node(visitor, node, *args): - if node._set in ComponentSet(visitor.setMap.keys()): + if node._set in visitor.setMap: # already detected set, do nothing pass else: - visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap.keys()) + 1) + visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) return '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( node._group, @@ -616,15 +616,15 @@ def latex_printer( # Cody's backdoor because he got outvoted if latex_component_map is not None: - if 'use_short_descriptors' in list(latex_component_map.keys()): + if 'use_short_descriptors' in latex_component_map: if latex_component_map['use_short_descriptors'] == False: use_short_descriptors = False if latex_component_map is None: latex_component_map = ComponentMap() - existing_components = ComponentSet([]) + existing_components = ComponentSet() else: - existing_components = ComponentSet(list(latex_component_map.keys())) + existing_components = ComponentSet(latex_component_map) isSingle = False @@ -1225,14 +1225,14 @@ def latex_printer( ) for ky, vl in new_variableMap.items(): - if ky not in ComponentSet(latex_component_map.keys()): + if ky not in latex_component_map: latex_component_map[ky] = vl for ky, vl in new_parameterMap.items(): - if ky not in ComponentSet(latex_component_map.keys()): + if ky not in latex_component_map: latex_component_map[ky] = vl rep_dict = {} - for ky in ComponentSet(list(reversed(list(latex_component_map.keys())))): + for ky in reversed(list(latex_component_map)): if isinstance(ky, (pyo.Var, _GeneralVarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: From ff111df42ac8aa4ff87377ab3fd16612a91b0eda Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:04:18 -0700 Subject: [PATCH 0590/1178] resolve indextemplate naming for multidimensional sets --- pyomo/contrib/latex_printer/latex_printer.py | 153 +++++++++++-------- 1 file changed, 88 insertions(+), 65 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 41cff29ad80..90a5da0d9c1 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -281,8 +281,9 @@ def handle_indexTemplate_node(visitor, node, *args): else: visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) - return '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( + return '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( node._group, + node._id, visitor.setMap[node._set], ) @@ -304,8 +305,9 @@ def handle_numericGetItemExpression_node(visitor, node, *args): def handle_templateSumExpression_node(visitor, node, *args): pstr = '' for i in range(0, len(node._iters)): - pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s__} ' % ( + pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__} ' % ( node._iters[i][0]._group, + ','.join(str(it._id) for it in node._iters[i]), visitor.setMap[node._iters[i][0]._set], ) @@ -904,24 +906,33 @@ def latex_printer( # setMap = visitor.setMap # Multiple constraints are generated using a set if len(indices) > 0: - if indices[0]._set in ComponentSet(visitor.setMap.keys()): - # already detected set, do nothing - pass - else: - visitor.setMap[indices[0]._set] = 'SET%d' % ( - len(visitor.setMap.keys()) + 1 + conLine += ' \\qquad \\forall' + + _bygroups = {} + for idx in indices: + _bygroups.setdefault(idx._group, []).append(idx) + for _group, idxs in _bygroups.items(): + if idxs[0]._set in visitor.setMap: + # already detected set, do nothing + pass + else: + visitor.setMap[idxs[0]._set] = 'SET%d' % ( + len(visitor.setMap) + 1 + ) + + idxTag = ','.join( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' + % (idx._group, idx._id, visitor.setMap[idx._set]) + for idx in idxs ) - idxTag = '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( - indices[0]._group, - visitor.setMap[indices[0]._set], - ) - setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( - indices[0]._group, - visitor.setMap[indices[0]._set], - ) + setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + indices[0]._group, + ','.join(str(it._id) for it in idxs), + visitor.setMap[indices[0]._set], + ) - conLine += ' \\qquad \\forall %s \\in %s ' % (idxTag, setTag) + conLine += ' %s \\in %s ' % (idxTag, setTag) pstr += conLine # Add labels as needed @@ -1070,8 +1081,8 @@ def latex_printer( for word in splitLatex: if "PLACEHOLDER_8675309_GROUP_" in word: ifo = word.split("PLACEHOLDER_8675309_GROUP_")[1] - gpNum, stName = ifo.split('_') - if gpNum not in groupMap.keys(): + gpNum, idNum, stName = ifo.split('_') + if gpNum not in groupMap: groupMap[gpNum] = [stName] if stName not in ComponentSet(uniqueSets): uniqueSets.append(stName) @@ -1088,10 +1099,7 @@ def latex_printer( ix = int(ky[3:]) - 1 setInfo[ky]['setObject'] = setMap_inverse[ky] # setList[ix] setInfo[ky]['setRegEx'] = ( - r'__S_PLACEHOLDER_8675309_GROUP_([0-9*])_%s__' % (ky) - ) - setInfo[ky]['sumSetRegEx'] = ( - r'sum_{__S_PLACEHOLDER_8675309_GROUP_([0-9*])_%s__}' % (ky) + r'__S_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9,]+)_%s__' % (ky,) ) # setInfo[ky]['idxRegEx'] = r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_%s__'%(ky) @@ -1116,27 +1124,41 @@ def latex_printer( ed = stData[-1] replacement = ( - r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_%s__ = %d }^{%d}' + r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_\2_%s__ = %d }^{%d}' % (ky, bgn, ed) ) - ln = re.sub(setInfo[ky]['sumSetRegEx'], replacement, ln) + ln = re.sub( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', replacement, ln + ) else: # if the set is not continuous or the flag has not been set - replacement = ( - r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_%s__ \\in __S_PLACEHOLDER_8675309_GROUP_\1_%s__ }' - % (ky, ky) - ) - ln = re.sub(setInfo[ky]['sumSetRegEx'], replacement, ln) + for _grp, _id in re.findall( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', ln + ): + set_placeholder = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + _grp, + _id, + ky, + ) + i_placeholder = ','.join( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (_grp, _, ky) + for _ in _id.split(',') + ) + replacement = r'sum_{ %s \in %s }' % ( + i_placeholder, + set_placeholder, + ) + ln = ln.replace('sum_{' + set_placeholder + '}', replacement) replacement = repr(defaultSetLatexNames[setInfo[ky]['setObject']])[1:-1] ln = re.sub(setInfo[ky]['setRegEx'], replacement, ln) # groupNumbers = re.findall(r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET[0-9]*__',ln) setNumbers = re.findall( - r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_SET([0-9]*)__', ln + r'__I_PLACEHOLDER_8675309_GROUP_[0-9]+_[0-9]+_SET([0-9]+)__', ln ) - groupSetPairs = re.findall( - r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET([0-9]*)__', ln + groupIdSetTuples = re.findall( + r'__I_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9]+)_SET([0-9]+)__', ln ) groupInfo = {} @@ -1146,43 +1168,44 @@ def latex_printer( 'indices': [], } - for gp in groupSetPairs: - if gp[0] not in groupInfo['SET' + gp[1]]['indices']: - groupInfo['SET' + gp[1]]['indices'].append(gp[0]) + for _gp, _id, _set in groupIdSetTuples: + if (_gp, _id) not in groupInfo['SET' + _set]['indices']: + groupInfo['SET' + _set]['indices'].append((_gp, _id)) + + def get_index_names(st, lcm): + if st in lcm: + return lcm[st][1] + elif isinstance(st, SetOperator): + return sum( + (get_index_names(s, lcm) for s in st.subsets(False)), start=[] + ) + elif st.dimen is not None: + return [None] * st.dimen + else: + return [Ellipsis] indexCounter = 0 for ky, vl in groupInfo.items(): - if vl['setObject'] in ComponentSet(latex_component_map.keys()): - indexNames = latex_component_map[vl['setObject']][1] - if len(indexNames) != 0: - if len(indexNames) < len(vl['indices']): - raise ValueError( - 'Insufficient number of indices provided to the overwrite dictionary for set %s' - % (vl['setObject'].name) - ) - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - indexNames[i], - ) - else: - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - alphabetStringGenerator(indexCounter), - ) - indexCounter += 1 - else: - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - alphabetStringGenerator(indexCounter), + indexNames = get_index_names(vl['setObject'], latex_component_map) + nonNone = list(filter(None, indexNames)) + if nonNone: + if len(nonNone) < len(vl['indices']): + raise ValueError( + 'Insufficient number of indices provided to the ' + 'overwrite dictionary for set %s (expected %s, but got %s)' + % (vl['setObject'].name, len(vl['indices']), indexNames) ) + else: + indexNames = [] + for i in vl['indices']: + indexNames.append(alphabetStringGenerator(indexCounter)) indexCounter += 1 - + for i in range(0, len(vl['indices'])): + ln = ln.replace( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' + % (*vl['indices'][i], ky), + indexNames[i], + ) latexLines[jj] = ln pstr = '\n'.join(latexLines) From e2e8165b731f24564a689a0f035797b621e78942 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:04:53 -0700 Subject: [PATCH 0591/1178] make it easier to switch mathds/mathbb --- pyomo/contrib/latex_printer/latex_printer.py | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 90a5da0d9c1..f3ffe2e5982 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -406,25 +406,28 @@ def exitNode(self, node, data): ) +mathbb = r'\mathbb' + + def analyze_variable(vr): domainMap = { - 'Reals': '\\mathds{R}', - 'PositiveReals': '\\mathds{R}_{> 0}', - 'NonPositiveReals': '\\mathds{R}_{\\leq 0}', - 'NegativeReals': '\\mathds{R}_{< 0}', - 'NonNegativeReals': '\\mathds{R}_{\\geq 0}', - 'Integers': '\\mathds{Z}', - 'PositiveIntegers': '\\mathds{Z}_{> 0}', - 'NonPositiveIntegers': '\\mathds{Z}_{\\leq 0}', - 'NegativeIntegers': '\\mathds{Z}_{< 0}', - 'NonNegativeIntegers': '\\mathds{Z}_{\\geq 0}', + 'Reals': mathbb + '{R}', + 'PositiveReals': mathbb + '{R}_{> 0}', + 'NonPositiveReals': mathbb + '{R}_{\\leq 0}', + 'NegativeReals': mathbb + '{R}_{< 0}', + 'NonNegativeReals': mathbb + '{R}_{\\geq 0}', + 'Integers': mathbb + '{Z}', + 'PositiveIntegers': mathbb + '{Z}_{> 0}', + 'NonPositiveIntegers': mathbb + '{Z}_{\\leq 0}', + 'NegativeIntegers': mathbb + '{Z}_{< 0}', + 'NonNegativeIntegers': mathbb + '{Z}_{\\geq 0}', 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', 'Binary': '\\left\\{ 0 , 1 \\right \\}', # 'Any': None, # 'AnyWithNone': None, 'EmptySet': '\\varnothing', - 'UnitInterval': '\\mathds{R}', - 'PercentFraction': '\\mathds{R}', + 'UnitInterval': mathbb + '{R}', + 'PercentFraction': mathbb + '{R}', # 'RealInterval' : None , # 'IntegerInterval' : None , } From 99c1bc319f281215fe42260af3da0296719dc650 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:05:59 -0700 Subject: [PATCH 0592/1178] Resolve issue with ambiguous field codes (when >10 vars or params) --- pyomo/contrib/latex_printer/latex_printer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index f3ffe2e5982..77afeb8f849 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -776,12 +776,12 @@ def latex_printer( for vr in variableList: vrIdx += 1 if isinstance(vr, ScalarVar): - variableMap[vr] = 'x_' + str(vrIdx) + variableMap[vr] = 'x_' + str(vrIdx) + '_' elif isinstance(vr, IndexedVar): - variableMap[vr] = 'x_' + str(vrIdx) + variableMap[vr] = 'x_' + str(vrIdx) + '_' for sd in vr.index_set().data(): vrIdx += 1 - variableMap[vr[sd]] = 'x_' + str(vrIdx) + variableMap[vr[sd]] = 'x_' + str(vrIdx) + '_' else: raise DeveloperError( 'Variable is not a variable. Should not happen. Contact developers' @@ -793,12 +793,12 @@ def latex_printer( for vr in parameterList: pmIdx += 1 if isinstance(vr, ScalarParam): - parameterMap[vr] = 'p_' + str(pmIdx) + parameterMap[vr] = 'p_' + str(pmIdx) + '_' elif isinstance(vr, IndexedParam): - parameterMap[vr] = 'p_' + str(pmIdx) + parameterMap[vr] = 'p_' + str(pmIdx) + '_' for sd in vr.index_set().data(): pmIdx += 1 - parameterMap[vr[sd]] = 'p_' + str(pmIdx) + parameterMap[vr[sd]] = 'p_' + str(pmIdx) + '_' else: raise DeveloperError( 'Parameter is not a parameter. Should not happen. Contact developers' From 28db0387d398c96af331741820d1715f210d0cdc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:25:07 -0700 Subject: [PATCH 0593/1178] Support name generation for set expressions --- pyomo/contrib/latex_printer/latex_printer.py | 81 ++++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 77afeb8f849..e41cbeac51e 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -49,7 +49,7 @@ ) from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam -from pyomo.core.base.set import _SetData +from pyomo.core.base.set import _SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint from pyomo.common.collections.component_map import ComponentMap from pyomo.common.collections.component_set import ComponentSet @@ -79,6 +79,39 @@ from pyomo.common.dependencies import numpy as np, numpy_available +set_operator_map = { + '|': r' \cup ', + '&': r' \cap ', + '*': r' \times ', + '-': r' \setminus ', + '^': r' \triangle ', +} + +latex_reals = r'\mathds{R}' +latex_integers = r'\mathds{Z}' + +domainMap = { + 'Reals': latex_reals, + 'PositiveReals': latex_reals + '_{> 0}', + 'NonPositiveReals': latex_reals + '_{\\leq 0}', + 'NegativeReals': latex_reals + '_{< 0}', + 'NonNegativeReals': latex_reals + '_{\\geq 0}', + 'Integers': latex_integers, + 'PositiveIntegers': latex_integers + '_{> 0}', + 'NonPositiveIntegers': latex_integers + '_{\\leq 0}', + 'NegativeIntegers': latex_integers + '_{< 0}', + 'NonNegativeIntegers': latex_integers + '_{\\geq 0}', + 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', + 'Binary': '\\left\\{ 0 , 1 \\right \\}', + # 'Any': None, + # 'AnyWithNone': None, + 'EmptySet': '\\varnothing', + 'UnitInterval': latex_reals, + 'PercentFraction': latex_reals, + # 'RealInterval' : None , + # 'IntegerInterval' : None , +} + def decoder(num, base): if int(num) != abs(num): # Requiring an integer is nice, but not strictly necessary; @@ -406,32 +439,7 @@ def exitNode(self, node, data): ) -mathbb = r'\mathbb' - - def analyze_variable(vr): - domainMap = { - 'Reals': mathbb + '{R}', - 'PositiveReals': mathbb + '{R}_{> 0}', - 'NonPositiveReals': mathbb + '{R}_{\\leq 0}', - 'NegativeReals': mathbb + '{R}_{< 0}', - 'NonNegativeReals': mathbb + '{R}_{\\geq 0}', - 'Integers': mathbb + '{Z}', - 'PositiveIntegers': mathbb + '{Z}_{> 0}', - 'NonPositiveIntegers': mathbb + '{Z}_{\\leq 0}', - 'NegativeIntegers': mathbb + '{Z}_{< 0}', - 'NonNegativeIntegers': mathbb + '{Z}_{\\geq 0}', - 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', - 'Binary': '\\left\\{ 0 , 1 \\right \\}', - # 'Any': None, - # 'AnyWithNone': None, - 'EmptySet': '\\varnothing', - 'UnitInterval': mathbb + '{R}', - 'PercentFraction': mathbb + '{R}', - # 'RealInterval' : None , - # 'IntegerInterval' : None , - } - domainName = vr.domain.name varBounds = vr.bounds lowerBoundValue = varBounds[0] @@ -1062,15 +1070,22 @@ def latex_printer( setMap = visitor.setMap setMap_inverse = {vl: ky for ky, vl in setMap.items()} + def generate_set_name(st, lcm): + if st in lcm: + return lcm[st][0] + if st.parent_block().component(st.name) is st: + return st.name.replace('_', r'\_') + if isinstance(st, SetOperator): + return _set_op_map[st._operator.strip()].join( + generate_set_name(s, lcm) for s in st.subsets(False) + ) + else: + return str(st).replace('_', r'\_').replace('{', '\{').replace('}', '\}') + # Handling the iterator indices defaultSetLatexNames = ComponentMap() - for ky, vl in setMap.items(): - st = ky - defaultSetLatexNames[st] = st.name.replace('_', '\\_') - if st in ComponentSet(latex_component_map.keys()): - defaultSetLatexNames[st] = latex_component_map[st][ - 0 - ] # .replace('_', '\\_') + for ky in setMap: + defaultSetLatexNames[ky] = generate_set_name(ky, latex_component_map) latexLines = pstr.split('\n') for jj in range(0, len(latexLines)): From 0d3400ffeeb93ed1764b4ba5f4eb487a309ecb35 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sun, 3 Mar 2024 05:09:56 -0700 Subject: [PATCH 0594/1178] add type hints to components --- pyomo/core/base/block.py | 22 ++++++++++++++++++++-- pyomo/core/base/constraint.py | 17 +++++++++++++++++ pyomo/core/base/indexed_component.py | 4 ++-- pyomo/core/base/param.py | 16 +++++++++++++++- pyomo/core/base/set.py | 16 +++++++++++++++- pyomo/core/base/var.py | 16 +++++++++++++++- 6 files changed, 84 insertions(+), 7 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index a0948c693d7..9ca2112d498 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import copy import logging import sys @@ -21,6 +22,7 @@ from io import StringIO from itertools import filterfalse, chain from operator import itemgetter, attrgetter +from typing import Union, Any, Type from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import Mapping @@ -44,6 +46,7 @@ from pyomo.core.base.indexed_component import ( ActiveIndexedComponent, UnindexedComponent_set, + IndexedComponent, ) from pyomo.opt.base import ProblemFormat, guess_format @@ -539,7 +542,7 @@ def __init__(self, component): super(_BlockData, self).__setattr__('_decl_order', []) self._private_data = None - def __getattr__(self, val): + def __getattr__(self, val) -> Union[Component, IndexedComponent, Any]: if val in ModelComponentFactory: return _component_decorator(self, ModelComponentFactory.get_class(val)) # Since the base classes don't support getattr, we can just @@ -548,7 +551,7 @@ def __getattr__(self, val): "'%s' object has no attribute '%s'" % (self.__class__.__name__, val) ) - def __setattr__(self, name, val): + def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): """ Set an attribute of a block data object. """ @@ -2007,6 +2010,18 @@ class Block(ActiveIndexedComponent): _ComponentDataClass = _BlockData _private_data_initializers = defaultdict(lambda: dict) + @overload + def __new__(cls: Type[Block], *args, **kwds) -> Union[ScalarBlock, IndexedBlock]: + ... + + @overload + def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: + ... + + @overload + def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: + ... + def __new__(cls, *args, **kwds): if cls != Block: return super(Block, cls).__new__(cls) @@ -2251,6 +2266,9 @@ class IndexedBlock(Block): def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) + def __getitem__(self, index) -> _BlockData: + return super().__getitem__(index) + # # Deprecated functions. diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8cf3c48ad0a..dcc90fd6280 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -9,10 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import sys import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload +from typing import Union, Type from pyomo.common.deprecation import RenamedClass from pyomo.common.errors import DeveloperError @@ -728,6 +730,18 @@ class Infeasible(object): Violated = Infeasible Satisfied = Feasible + @overload + def __new__(cls: Type[Constraint], *args, **kwds) -> Union[ScalarConstraint, IndexedConstraint]: + ... + + @overload + def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: + ... + + @overload + def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: + ... + def __new__(cls, *args, **kwds): if cls != Constraint: return super(Constraint, cls).__new__(cls) @@ -1019,6 +1033,9 @@ class IndexedConstraint(Constraint): def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) + + def __getitem__(self, index) -> _GeneralConstraintData: + return super().__getitem__(index) @ModelComponentFactory.register("A list of constraint expressions.") diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 0d498da091d..e1be613d666 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -18,7 +18,7 @@ import pyomo.core.base as BASE from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.initializer import Initializer -from pyomo.core.base.component import Component, ActiveComponent +from pyomo.core.base.component import Component, ActiveComponent, ComponentData from pyomo.core.base.config import PyomoOptions from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_set @@ -606,7 +606,7 @@ def iteritems(self): """Return a list (index,data) tuples from the dictionary""" return self.items() - def __getitem__(self, index): + def __getitem__(self, index) -> ComponentData: """ This method returns the data corresponding to the given index. """ diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 3ef33b9ee45..dde390661ab 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -9,11 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import sys import types import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload +from typing import Union, Type from pyomo.common.autoslots import AutoSlots from pyomo.common.deprecation import deprecation_warning, RenamedClass @@ -291,6 +293,18 @@ class NoValue(object): pass + @overload + def __new__(cls: Type[Param], *args, **kwds) -> Union[ScalarParam, IndexedParam]: + ... + + @overload + def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: + ... + + @overload + def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: + ... + def __new__(cls, *args, **kwds): if cls != Param: return super(Param, cls).__new__(cls) @@ -983,7 +997,7 @@ def _create_objects_for_deepcopy(self, memo, component_list): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args): + def __getitem__(self, args) -> _ParamData: try: return super().__getitem__(args) except: diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 2dc14460911..c52945dfd30 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import inspect import itertools import logging @@ -16,6 +17,8 @@ import sys import weakref from pyomo.common.pyomo_typing import overload +from typing import Union, Type, Any +from collections.abc import Iterator from pyomo.common.collections import ComponentSet from pyomo.common.deprecation import deprecated, deprecation_warning, RenamedClass @@ -569,7 +572,7 @@ def isordered(self): def subsets(self, expand_all_set_operators=None): return iter((self,)) - def __iter__(self): + def __iter__(self) -> Iterator[Any]: """Iterate over the set members Raises AttributeError for non-finite sets. This must be @@ -1967,6 +1970,14 @@ class SortedOrder(object): _ValidOrderedAuguments = {True, False, InsertionOrder, SortedOrder} _UnorderedInitializers = {set} + @overload + def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: + ... + + @overload + def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: + ... + def __new__(cls, *args, **kwds): if cls is not Set: return super(Set, cls).__new__(cls) @@ -2373,6 +2384,9 @@ def data(self): "Return a dict containing the data() of each Set in this IndexedSet" return {k: v.data() for k, v in self.items()} + def __getitem__(self, index) -> _SetData: + return super().__getitem__(index) + class FiniteScalarSet(_FiniteSetData, Set): def __init__(self, **kwds): diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index f426c9c4f55..c92a4056667 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -9,10 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import logging import sys from pyomo.common.pyomo_typing import overload from weakref import ref as weakref_ref +from typing import Union, Type from pyomo.common.deprecation import RenamedClass from pyomo.common.log import is_debug_set @@ -668,6 +670,18 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): _ComponentDataClass = _GeneralVarData + @overload + def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: + ... + + @overload + def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: + ... + + @overload + def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: + ... + def __new__(cls, *args, **kwargs): if cls is not Var: return super(Var, cls).__new__(cls) @@ -1046,7 +1060,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args): + def __getitem__(self, args) -> _GeneralVarData: try: return super().__getitem__(args) except RuntimeError: From ae5ddeb36428d0a77adeccbd15f9daa6fcce7c4e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sun, 3 Mar 2024 05:13:01 -0700 Subject: [PATCH 0595/1178] run black --- pyomo/core/base/block.py | 11 +++++------ pyomo/core/base/constraint.py | 13 ++++++------- pyomo/core/base/param.py | 11 +++++------ pyomo/core/base/set.py | 6 ++---- pyomo/core/base/var.py | 11 ++++------- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 9ca2112d498..908e0ef1abd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2011,16 +2011,15 @@ class Block(ActiveIndexedComponent): _private_data_initializers = defaultdict(lambda: dict) @overload - def __new__(cls: Type[Block], *args, **kwds) -> Union[ScalarBlock, IndexedBlock]: - ... + def __new__( + cls: Type[Block], *args, **kwds + ) -> Union[ScalarBlock, IndexedBlock]: ... @overload - def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: - ... + def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: ... @overload - def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: - ... + def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: ... def __new__(cls, *args, **kwds): if cls != Block: diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index dcc90fd6280..a36bc679e49 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -731,16 +731,15 @@ class Infeasible(object): Satisfied = Feasible @overload - def __new__(cls: Type[Constraint], *args, **kwds) -> Union[ScalarConstraint, IndexedConstraint]: - ... + def __new__( + cls: Type[Constraint], *args, **kwds + ) -> Union[ScalarConstraint, IndexedConstraint]: ... @overload - def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: - ... + def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: ... @overload - def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: - ... + def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: ... def __new__(cls, *args, **kwds): if cls != Constraint: @@ -1033,7 +1032,7 @@ class IndexedConstraint(Constraint): def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) - + def __getitem__(self, index) -> _GeneralConstraintData: return super().__getitem__(index) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index dde390661ab..5fcaf92b25a 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -294,16 +294,15 @@ class NoValue(object): pass @overload - def __new__(cls: Type[Param], *args, **kwds) -> Union[ScalarParam, IndexedParam]: - ... + def __new__( + cls: Type[Param], *args, **kwds + ) -> Union[ScalarParam, IndexedParam]: ... @overload - def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: - ... + def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: ... @overload - def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: - ... + def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: ... def __new__(cls, *args, **kwds): if cls != Param: diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c52945dfd30..6373af97683 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1971,12 +1971,10 @@ class SortedOrder(object): _UnorderedInitializers = {set} @overload - def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: - ... + def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: ... @overload - def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: - ... + def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... def __new__(cls, *args, **kwds): if cls is not Set: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index c92a4056667..856a2dc0237 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -671,16 +671,13 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): _ComponentDataClass = _GeneralVarData @overload - def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: - ... + def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... @overload - def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: - ... + def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: ... @overload - def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: - ... + def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: ... def __new__(cls, *args, **kwargs): if cls is not Var: @@ -702,7 +699,7 @@ def __init__( dense=True, units=None, name=None, - doc=None + doc=None, ): ... def __init__(self, *args, **kwargs): From b28f4bb7179a614f2532b591e1ff38eb1df4ad6f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sun, 3 Mar 2024 05:20:32 -0700 Subject: [PATCH 0596/1178] name conflict --- pyomo/core/base/set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 6373af97683..b8ddae14e9f 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -17,7 +17,7 @@ import sys import weakref from pyomo.common.pyomo_typing import overload -from typing import Union, Type, Any +from typing import Union, Type, Any as typingAny from collections.abc import Iterator from pyomo.common.collections import ComponentSet @@ -572,7 +572,7 @@ def isordered(self): def subsets(self, expand_all_set_operators=None): return iter((self,)) - def __iter__(self) -> Iterator[Any]: + def __iter__(self) -> Iterator[typingAny]: """Iterate over the set members Raises AttributeError for non-finite sets. This must be From b1444017c3fd536b0630dda91f9290b6e3043798 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 3 Mar 2024 09:07:15 -0700 Subject: [PATCH 0597/1178] NFC: apply black --- pyomo/contrib/latex_printer/latex_printer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index e41cbeac51e..c2cbfd6b2e1 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -112,6 +112,7 @@ # 'IntegerInterval' : None , } + def decoder(num, base): if int(num) != abs(num): # Requiring an integer is nice, but not strictly necessary; From 2273282c76d8cba84767ece983153445fa795ade Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 08:17:07 -0700 Subject: [PATCH 0598/1178] Try some different stuff to get more printouts --- .github/workflows/test_branches.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 77f47b505ff..661d86ef890 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -86,7 +86,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 @@ -333,10 +333,11 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + echo "" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) - conda install --update-deps -q -y $CONDA_DEPENDENCIES + conda install --update-deps -y $CONDA_DEPENDENCIES if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" From a32c0040b5c66ed524966a072d2ab22d5f6d9745 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:00:06 -0700 Subject: [PATCH 0599/1178] Revert to 3.9 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 661d86ef890..5dca79f294e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -86,7 +86,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: '3.10' + python: 3.9 other: /mpi mpi: 3 skip_doctest: 1 From ffa594cdf1e4bc7f621e73ae5a8bb94595cf6399 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:08:04 -0700 Subject: [PATCH 0600/1178] Back to 3.10 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5dca79f294e..661d86ef890 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -86,7 +86,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 From e446941c9d2d965b6b2cb4744eb01f03de93f67c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:11:00 -0700 Subject: [PATCH 0601/1178] Upgrade to macos-13 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 661d86ef890..89c1fbeb7e4 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -66,7 +66,7 @@ jobs: TARGET: linux PYENV: pip - - os: macos-latest + - os: macos-13 python: '3.10' TARGET: osx PYENV: pip From a4a1fd41a70708eb7179b421e67d9c739a016793 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:23:16 -0700 Subject: [PATCH 0602/1178] Lower to 2 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89c1fbeb7e4..6867043f67e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -88,7 +88,7 @@ jobs: - os: ubuntu-latest python: '3.10' other: /mpi - mpi: 3 + mpi: 2 skip_doctest: 1 TARGET: linux PYENV: conda From 4ae6a70a72b07b0f811ea8e72035937b7a111efb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 12:15:57 -0700 Subject: [PATCH 0603/1178] Add oversubscribe --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 6867043f67e..1cb6fd0926d 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -88,7 +88,7 @@ jobs: - os: ubuntu-latest python: '3.10' other: /mpi - mpi: 2 + mpi: 3 skip_doctest: 1 TARGET: linux PYENV: conda @@ -632,7 +632,7 @@ jobs: $PYTHON_EXE -c "from pyomo.dataportal.parse_datacmds import \ parse_data_commands; parse_data_commands(data='')" # Note: if we are testing with openmpi, add '--oversubscribe' - mpirun -np ${{matrix.mpi}} pytest -v \ + mpirun -np ${{matrix.mpi}} -oversubscribe pytest -v \ --junit-xml=TEST-pyomo-mpi.xml \ -m "mpi" -W ignore::Warning \ pyomo `pwd`/pyomo-model-libraries diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 87d6aa4d7a8..e3e08847aa9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -59,7 +59,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] python: [ 3.8, 3.9, '3.10', '3.11', '3.12' ] other: [""] category: [""] @@ -69,7 +69,7 @@ jobs: TARGET: linux PYENV: pip - - os: macos-latest + - os: macos-13 TARGET: osx PYENV: pip @@ -87,7 +87,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 @@ -661,7 +661,7 @@ jobs: $PYTHON_EXE -c "from pyomo.dataportal.parse_datacmds import \ parse_data_commands; parse_data_commands(data='')" # Note: if we are testing with openmpi, add '--oversubscribe' - mpirun -np ${{matrix.mpi}} pytest -v \ + mpirun -np ${{matrix.mpi}} -oversubscribe pytest -v \ --junit-xml=TEST-pyomo-mpi.xml \ -m "mpi" -W ignore::Warning \ pyomo `pwd`/pyomo-model-libraries From 1e609c067dc01e7cd6ea8c65a347e540f0534d02 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 13:54:23 -0700 Subject: [PATCH 0604/1178] run black --- pyomo/contrib/simplification/simplify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index b8cc4995f91..80e7c42549c 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -55,7 +55,11 @@ def simplify(self, expr: NumericExpression): return simplify_with_ginac(expr, self.gi) else: if not self.suppress_no_ginac_warnings: - msg = f"GiNaC does not seem to be available. Using SymPy. Note that the GiNac interface is significantly faster." + msg = ( + "GiNaC does not seem to be available. Using SymPy. " + + "Note that the GiNac interface is significantly faster." + ) logger.warning(msg) warnings.warn(msg) + self.suppress_no_ginac_warnings = True return simplify_with_sympy(expr) From 42fd802267b5f4e25606f844a966cceb569cbdd7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 14:20:57 -0700 Subject: [PATCH 0605/1178] Change macos for coverage upload as well --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 1cb6fd0926d..55f903a37f9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -709,12 +709,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] include: - os: ubuntu-latest TARGET: linux - - os: macos-latest + - os: macos-13 TARGET: osx - os: windows-latest TARGET: win diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index e3e08847aa9..76ec6de951a 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -739,12 +739,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] include: - os: ubuntu-latest TARGET: linux - - os: macos-latest + - os: macos-13 TARGET: osx - os: windows-latest TARGET: win From ada10bd444d93aad724d665cbaf890a8badbd6cd Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 16:31:20 -0700 Subject: [PATCH 0606/1178] only modify module __path__ and __spec__ for deferred import modules --- pyomo/common/dependencies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 472b0011edb..22d15749879 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -902,10 +902,10 @@ def __exit__(self, exc_type, exc_value, traceback): sys.modules[_global_name + name] = sys.modules[name] while deferred: name, mod = deferred.popitem() - mod.__path__ = None - mod.__spec__ = None - sys.modules[_global_name + name] = mod if isinstance(mod, DeferredImportModule): + mod.__path__ = None + mod.__spec__ = None + sys.modules[_global_name + name] = mod deferred.update( (name + '.' + k, v) for k, v in mod.__dict__.items() From df94ec7786894a38e528d4802b5ff6ecf23ca1dc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 17:06:08 -0700 Subject: [PATCH 0607/1178] deferred import fix --- pyomo/common/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 22d15749879..ea9efe370f7 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -902,10 +902,10 @@ def __exit__(self, exc_type, exc_value, traceback): sys.modules[_global_name + name] = sys.modules[name] while deferred: name, mod = deferred.popitem() + sys.modules[_global_name + name] = mod if isinstance(mod, DeferredImportModule): mod.__path__ = None mod.__spec__ = None - sys.modules[_global_name + name] = mod deferred.update( (name + '.' + k, v) for k, v in mod.__dict__.items() From 8ee01941a433eab987e6fa11ffa74a6b7ea5f2bb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 12:38:32 -0700 Subject: [PATCH 0608/1178] Add tests for set products --- pyomo/contrib/latex_printer/latex_printer.py | 2 +- .../latex_printer/tests/test_latex_printer.py | 63 +++++++++++++---- pyomo/core/tests/examples/pmedian_concrete.py | 70 +++++++++++++++++++ 3 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 pyomo/core/tests/examples/pmedian_concrete.py diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index c2cbfd6b2e1..1d5279e984a 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1077,7 +1077,7 @@ def generate_set_name(st, lcm): if st.parent_block().component(st.name) is st: return st.name.replace('_', r'\_') if isinstance(st, SetOperator): - return _set_op_map[st._operator.strip()].join( + return set_operator_map[st._operator.strip()].join( generate_set_name(s, lcm) for s in st.subsets(False) ) else: diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index 2d7dd69dba8..f09a14b8b00 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -9,25 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import io +from textwrap import dedent + import pyomo.common.unittest as unittest -from pyomo.contrib.latex_printer import latex_printer +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete import pyomo.environ as pyo -from textwrap import dedent + +from pyomo.contrib.latex_printer import latex_printer from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections.component_map import ComponentMap - from pyomo.environ import ( Reals, PositiveReals, @@ -797,6 +788,50 @@ def ruleMaker_2(m, i): self.assertEqual('\n' + pstr + '\n', bstr) + def test_latexPrinter_pmedian_verbose(self): + m = pmedian_concrete.create_model() + self.assertEqual( + latex_printer(m).strip(), + r""" +\begin{align} + & \min + & & \sum_{ i \in Locations } \sum_{ j \in Customers } cost_{i,j} serve\_customer\_from\_location_{i,j} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ i \in Locations } serve\_customer\_from\_location_{i,j} = 1 & \qquad \forall j \in Customers \label{con:M1_single_x} \\ + &&& serve\_customer\_from\_location_{i,j} \leq select\_location_{i} & \qquad \forall i,j \in Locations \times Customers \label{con:M1_bound_y} \\ + &&& \sum_{ i \in Locations } select\_location_{i} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ + &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} +\end{align} + """.strip() + ) + + def test_latexPrinter_pmedian_concise(self): + m = pmedian_concrete.create_model() + lcm = ComponentMap() + lcm[m.Locations] = ['L', ['n']] + lcm[m.Customers] = ['C', ['m']] + lcm[m.cost] = 'd' + lcm[m.serve_customer_from_location] = 'x' + lcm[m.select_location] = 'y' + self.assertEqual( + latex_printer(m, latex_component_map=lcm).strip(), + r""" +\begin{align} + & \min + & & \sum_{ n \in L } \sum_{ m \in C } d_{n,m} x_{n,m} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ n \in L } x_{n,m} = 1 & \qquad \forall m \in C \label{con:M1_single_x} \\ + &&& x_{n,m} \leq y_{n} & \qquad \forall n,m \in L \times C \label{con:M1_bound_y} \\ + &&& \sum_{ n \in L } y_{n} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ + &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} +\end{align} + """.strip() + ) + if __name__ == '__main__': unittest.main() diff --git a/pyomo/core/tests/examples/pmedian_concrete.py b/pyomo/core/tests/examples/pmedian_concrete.py new file mode 100644 index 00000000000..a6a1859df23 --- /dev/null +++ b/pyomo/core/tests/examples/pmedian_concrete.py @@ -0,0 +1,70 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math +from pyomo.environ import ( + ConcreteModel, + Param, + RangeSet, + Var, + Reals, + Binary, + PositiveIntegers, +) + + +def _cost_rule(model, n, m): + # We will assume costs are an arbitrary function of the indices + return math.sin(n * 2.33333 + m * 7.99999) + + +def create_model(n=3, m=3, p=2): + model = ConcreteModel(name="M1") + + model.N = Param(initialize=n, within=PositiveIntegers) + model.M = Param(initialize=m, within=PositiveIntegers) + model.P = Param(initialize=p, within=RangeSet(1, model.N), mutable=True) + + model.Locations = RangeSet(1, model.N) + model.Customers = RangeSet(1, model.M) + + model.cost = Param( + model.Locations, model.Customers, initialize=_cost_rule, within=Reals + ) + model.serve_customer_from_location = Var( + model.Locations, model.Customers, bounds=(0.0, 1.0) + ) + model.select_location = Var(model.Locations, within=Binary) + + @model.Objective() + def obj(model): + return sum( + model.cost[n, m] * model.serve_customer_from_location[n, m] + for n in model.Locations + for m in model.Customers + ) + + @model.Constraint(model.Customers) + def single_x(model, m): + return ( + sum(model.serve_customer_from_location[n, m] for n in model.Locations) + == 1.0 + ) + + @model.Constraint(model.Locations, model.Customers) + def bound_y(model, n, m): + return model.serve_customer_from_location[n, m] <= model.select_location[n] + + @model.Constraint() + def num_facilities(model): + return sum(model.select_location[n] for n in model.Locations) == model.P + + return model From 30cb7e4b6daa82430eab415ae5cb603cd849ccf1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 12:49:29 -0700 Subject: [PATCH 0609/1178] NFC: apply black --- pyomo/contrib/latex_printer/tests/test_latex_printer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index f09a14b8b00..b0ada97a5fe 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -13,7 +13,7 @@ from textwrap import dedent import pyomo.common.unittest as unittest -import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete import pyomo.environ as pyo from pyomo.contrib.latex_printer import latex_printer @@ -804,7 +804,7 @@ def test_latexPrinter_pmedian_verbose(self): & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} \end{align} - """.strip() + """.strip(), ) def test_latexPrinter_pmedian_concise(self): @@ -829,7 +829,7 @@ def test_latexPrinter_pmedian_concise(self): & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} \end{align} - """.strip() + """.strip(), ) From 9d9c98ad02699535f0a446fb90683f8a3356f3b4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:05:43 -0700 Subject: [PATCH 0610/1178] simplify initialization of exit node dispatchers --- pyomo/repn/linear.py | 63 ++++++++--------------------- pyomo/repn/quadratic.py | 89 ++++++++--------------------------------- 2 files changed, 33 insertions(+), 119 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 6ab4abfdaf5..68dda60d3c0 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -208,9 +208,8 @@ def _handle_negation_ANY(visitor, node, arg): _exit_node_handlers[NegationExpression] = { + None: _handle_negation_ANY, (_CONSTANT,): _handle_negation_constant, - (_LINEAR,): _handle_negation_ANY, - (_GENERAL,): _handle_negation_ANY, } # @@ -284,15 +283,12 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression] = { + None: _handle_product_nonlinear, (_CONSTANT, _CONSTANT): _handle_product_constant_constant, (_CONSTANT, _LINEAR): _handle_product_constant_ANY, (_CONSTANT, _GENERAL): _handle_product_constant_ANY, (_LINEAR, _CONSTANT): _handle_product_ANY_constant, - (_LINEAR, _LINEAR): _handle_product_nonlinear, - (_LINEAR, _GENERAL): _handle_product_nonlinear, (_GENERAL, _CONSTANT): _handle_product_ANY_constant, - (_GENERAL, _LINEAR): _handle_product_nonlinear, - (_GENERAL, _GENERAL): _handle_product_nonlinear, } _exit_node_handlers[MonomialTermExpression] = _exit_node_handlers[ProductExpression] @@ -317,15 +313,10 @@ def _handle_division_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[DivisionExpression] = { + None: _handle_division_nonlinear, (_CONSTANT, _CONSTANT): _handle_division_constant_constant, - (_CONSTANT, _LINEAR): _handle_division_nonlinear, - (_CONSTANT, _GENERAL): _handle_division_nonlinear, (_LINEAR, _CONSTANT): _handle_division_ANY_constant, - (_LINEAR, _LINEAR): _handle_division_nonlinear, - (_LINEAR, _GENERAL): _handle_division_nonlinear, (_GENERAL, _CONSTANT): _handle_division_ANY_constant, - (_GENERAL, _LINEAR): _handle_division_nonlinear, - (_GENERAL, _GENERAL): _handle_division_nonlinear, } # @@ -366,15 +357,10 @@ def _handle_pow_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[PowExpression] = { + None: _handle_pow_nonlinear, (_CONSTANT, _CONSTANT): _handle_pow_constant_constant, - (_CONSTANT, _LINEAR): _handle_pow_nonlinear, - (_CONSTANT, _GENERAL): _handle_pow_nonlinear, (_LINEAR, _CONSTANT): _handle_pow_ANY_constant, - (_LINEAR, _LINEAR): _handle_pow_nonlinear, - (_LINEAR, _GENERAL): _handle_pow_nonlinear, (_GENERAL, _CONSTANT): _handle_pow_ANY_constant, - (_GENERAL, _LINEAR): _handle_pow_nonlinear, - (_GENERAL, _GENERAL): _handle_pow_nonlinear, } # @@ -397,9 +383,8 @@ def _handle_unary_nonlinear(visitor, node, arg): _exit_node_handlers[UnaryFunctionExpression] = { + None: _handle_unary_nonlinear, (_CONSTANT,): _handle_unary_constant, - (_LINEAR,): _handle_unary_nonlinear, - (_GENERAL,): _handle_unary_nonlinear, } _exit_node_handlers[AbsExpression] = _exit_node_handlers[UnaryFunctionExpression] @@ -422,9 +407,8 @@ def _handle_named_ANY(visitor, node, arg1): _exit_node_handlers[Expression] = { + None: _handle_named_ANY, (_CONSTANT,): _handle_named_constant, - (_LINEAR,): _handle_named_ANY, - (_GENERAL,): _handle_named_ANY, } # @@ -457,12 +441,7 @@ def _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3): return _GENERAL, ans -_exit_node_handlers[Expr_ifExpression] = { - (i, j, k): _handle_expr_if_nonlinear - for i in (_LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) - for k in (_CONSTANT, _LINEAR, _GENERAL) -} +_exit_node_handlers[Expr_ifExpression] = {None: _handle_expr_if_nonlinear} for j in (_CONSTANT, _LINEAR, _GENERAL): for k in (_CONSTANT, _LINEAR, _GENERAL): _exit_node_handlers[Expr_ifExpression][_CONSTANT, j, k] = _handle_expr_if_const @@ -495,11 +474,9 @@ def _handle_equality_general(visitor, node, arg1, arg2): _exit_node_handlers[EqualityExpression] = { - (i, j): _handle_equality_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_equality_general, + (_CONSTANT, _CONSTANT): _handle_equality_const, } -_exit_node_handlers[EqualityExpression][_CONSTANT, _CONSTANT] = _handle_equality_const def _handle_inequality_const(visitor, node, arg1, arg2): @@ -525,13 +502,9 @@ def _handle_inequality_general(visitor, node, arg1, arg2): _exit_node_handlers[InequalityExpression] = { - (i, j): _handle_inequality_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_inequality_general, + (_CONSTANT, _CONSTANT): _handle_inequality_const, } -_exit_node_handlers[InequalityExpression][ - _CONSTANT, _CONSTANT -] = _handle_inequality_const def _handle_ranged_const(visitor, node, arg1, arg2, arg3): @@ -562,14 +535,9 @@ def _handle_ranged_general(visitor, node, arg1, arg2, arg3): _exit_node_handlers[RangedExpression] = { - (i, j, k): _handle_ranged_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) - for k in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_ranged_general, + (_CONSTANT, _CONSTANT, _CONSTANT): _handle_ranged_const, } -_exit_node_handlers[RangedExpression][ - _CONSTANT, _CONSTANT, _CONSTANT -] = _handle_ranged_const class LinearBeforeChildDispatcher(BeforeChildDispatcher): @@ -750,7 +718,10 @@ def _initialize_exit_node_dispatcher(exit_handlers): exit_dispatcher = {} for cls, handlers in exit_handlers.items(): for args, fcn in handlers.items(): - exit_dispatcher[(cls, *args)] = fcn + if args is None: + exit_dispatcher[cls] = fcn + else: + exit_dispatcher[(cls, *args)] = fcn return exit_dispatcher diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index c538d1efc7f..2ff3276f7f5 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -284,18 +284,11 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression].update( { + None: _handle_product_nonlinear, (_CONSTANT, _QUADRATIC): linear._handle_product_constant_ANY, - (_LINEAR, _QUADRATIC): _handle_product_nonlinear, - (_QUADRATIC, _QUADRATIC): _handle_product_nonlinear, - (_GENERAL, _QUADRATIC): _handle_product_nonlinear, (_QUADRATIC, _CONSTANT): linear._handle_product_ANY_constant, - (_QUADRATIC, _LINEAR): _handle_product_nonlinear, - (_QUADRATIC, _GENERAL): _handle_product_nonlinear, # Replace handler from the linear walker (_LINEAR, _LINEAR): _handle_product_linear_linear, - (_GENERAL, _GENERAL): _handle_product_nonlinear, - (_GENERAL, _LINEAR): _handle_product_nonlinear, - (_LINEAR, _GENERAL): _handle_product_nonlinear, } ) @@ -303,15 +296,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): # DIVISION # _exit_node_handlers[DivisionExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_division_nonlinear, - (_LINEAR, _QUADRATIC): linear._handle_division_nonlinear, - (_QUADRATIC, _QUADRATIC): linear._handle_division_nonlinear, - (_GENERAL, _QUADRATIC): linear._handle_division_nonlinear, - (_QUADRATIC, _CONSTANT): linear._handle_division_ANY_constant, - (_QUADRATIC, _LINEAR): linear._handle_division_nonlinear, - (_QUADRATIC, _GENERAL): linear._handle_division_nonlinear, - } + {(_QUADRATIC, _CONSTANT): linear._handle_division_ANY_constant} ) @@ -319,84 +304,42 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): # EXPONENTIATION # _exit_node_handlers[PowExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_pow_nonlinear, - (_LINEAR, _QUADRATIC): linear._handle_pow_nonlinear, - (_QUADRATIC, _QUADRATIC): linear._handle_pow_nonlinear, - (_GENERAL, _QUADRATIC): linear._handle_pow_nonlinear, - (_QUADRATIC, _CONSTANT): linear._handle_pow_ANY_constant, - (_QUADRATIC, _LINEAR): linear._handle_pow_nonlinear, - (_QUADRATIC, _GENERAL): linear._handle_pow_nonlinear, - } + {(_QUADRATIC, _CONSTANT): linear._handle_pow_ANY_constant} ) # # ABS and UNARY handlers # -_exit_node_handlers[AbsExpression][(_QUADRATIC,)] = linear._handle_unary_nonlinear -_exit_node_handlers[UnaryFunctionExpression][ - (_QUADRATIC,) -] = linear._handle_unary_nonlinear +# (no changes needed) # # NAMED EXPRESSION handlers # -_exit_node_handlers[Expression][(_QUADRATIC,)] = linear._handle_named_ANY +# (no changes needed) # # EXPR_IF handlers # # Note: it is easier to just recreate the entire data structure, rather # than update it -_exit_node_handlers[Expr_ifExpression] = { - (i, j, k): linear._handle_expr_if_nonlinear - for i in (_LINEAR, _QUADRATIC, _GENERAL) - for j in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) - for k in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) -} -for j in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL): - for k in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL): - _exit_node_handlers[Expr_ifExpression][ - _CONSTANT, j, k - ] = linear._handle_expr_if_const - -# -# RELATIONAL handlers -# -_exit_node_handlers[EqualityExpression].update( +_exit_node_handlers[Expr_ifExpression].update( { - (_CONSTANT, _QUADRATIC): linear._handle_equality_general, - (_LINEAR, _QUADRATIC): linear._handle_equality_general, - (_QUADRATIC, _QUADRATIC): linear._handle_equality_general, - (_GENERAL, _QUADRATIC): linear._handle_equality_general, - (_QUADRATIC, _CONSTANT): linear._handle_equality_general, - (_QUADRATIC, _LINEAR): linear._handle_equality_general, - (_QUADRATIC, _GENERAL): linear._handle_equality_general, + (_CONSTANT, i, _QUADRATIC): linear._handle_expr_if_const + for i in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) } ) -_exit_node_handlers[InequalityExpression].update( +_exit_node_handlers[Expr_ifExpression].update( { - (_CONSTANT, _QUADRATIC): linear._handle_inequality_general, - (_LINEAR, _QUADRATIC): linear._handle_inequality_general, - (_QUADRATIC, _QUADRATIC): linear._handle_inequality_general, - (_GENERAL, _QUADRATIC): linear._handle_inequality_general, - (_QUADRATIC, _CONSTANT): linear._handle_inequality_general, - (_QUADRATIC, _LINEAR): linear._handle_inequality_general, - (_QUADRATIC, _GENERAL): linear._handle_inequality_general, - } -) -_exit_node_handlers[RangedExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_ranged_general, - (_LINEAR, _QUADRATIC): linear._handle_ranged_general, - (_QUADRATIC, _QUADRATIC): linear._handle_ranged_general, - (_GENERAL, _QUADRATIC): linear._handle_ranged_general, - (_QUADRATIC, _CONSTANT): linear._handle_ranged_general, - (_QUADRATIC, _LINEAR): linear._handle_ranged_general, - (_QUADRATIC, _GENERAL): linear._handle_ranged_general, + (_CONSTANT, _QUADRATIC, i): linear._handle_expr_if_const + for i in (_CONSTANT, _LINEAR, _GENERAL) } ) +# +# RELATIONAL handlers +# +# (no changes needed) + class QuadraticRepnVisitor(linear.LinearRepnVisitor): Result = QuadraticRepn From 81f6e273d5585bc7ca29d5b3531e0bdb91e2e8eb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:06:55 -0700 Subject: [PATCH 0611/1178] rework ExitNodeDispatcher.__missing__ to make default fallback cleaner --- pyomo/repn/util.py | 55 +++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 49cca32eaf9..7bba4041c70 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -401,6 +401,13 @@ def __init__(self, *args, **kwargs): def __missing__(self, key): if type(key) is tuple: node_class = key[0] + node_args = key[1:] + # Only lookup/cache argument-specific handlers for unary, + # binary and ternary operators + if len(key) > 3: + key = node_class + if key in self: + return self[key] else: node_class = key bases = node_class.__mro__ @@ -412,35 +419,33 @@ def __missing__(self, key): bases = [Expression] fcn = None for base_type in bases: - if isinstance(key, tuple): - base_key = (base_type,) + key[1:] - # Only cache handlers for unary, binary and ternary operators - cache = len(key) <= 4 - else: - base_key = base_type - cache = True - if base_key in self: - fcn = self[base_key] - elif base_type in self: + if key is not node_class: + if (base_type,) + node_args in self: + fcn = self[(base_type,) + node_args] + break + if base_type in self: fcn = self[base_type] - elif any((k[0] if type(k) is tuple else k) is base_type for k in self): - raise DeveloperError( - f"Base expression key '{base_key}' not found when inserting " - f"dispatcher for node '{node_class.__name__}' while walking " - "expression tree." - ) + break if fcn is None: - fcn = self.unexpected_expression_type - if cache: - self[key] = fcn + partial_matches = set( + k[0] for k in self if type(k) is tuple and issubclass(node_class, k[0]) + ) + for base_type in node_class.__mro__: + if node_class is not key: + key = (base_type,) + node_args + if base_type in partial_matches: + raise DeveloperError( + f"Base expression key '{key}' not found when inserting " + f"dispatcher for node '{node_class.__name__}' while walking " + "expression tree." + ) + raise DeveloperError( + f"Unexpected expression node type '{node_class.__name__}' " + f"found while walking expression tree in {type(self).__name__}." + ) + self[key] = fcn return fcn - def unexpected_expression_type(self, visitor, node, *arg): - raise DeveloperError( - f"Unexpected expression node type '{type(node).__name__}' " - f"found while walking expression tree in {type(visitor).__name__}." - ) - def apply_node_operation(node, args): try: From 4c9d44b46966372a4031ca66e9110f907fbc55e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:07:27 -0700 Subject: [PATCH 0612/1178] Minor dispatcher performance improvement --- pyomo/repn/linear.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 68dda60d3c0..ed1d8f6b32f 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -218,20 +218,18 @@ def _handle_negation_ANY(visitor, node, arg): def _handle_product_constant_constant(visitor, node, arg1, arg2): - _, arg1 = arg1 - _, arg2 = arg2 - ans = arg1 * arg2 + ans = arg1[1] * arg2[1] if ans != ans: - if not arg1 or not arg2: + if not arg1[1] or not arg2[1]: deprecation_warning( - f"Encountered {str(arg1)}*{str(arg2)} in expression tree. " + f"Encountered {str(arg1[1])}*{str(arg2[1])} in expression tree. " "Mapping the NaN result to 0 for compatibility " "with the lp_v1 writer. In the future, this NaN " "will be preserved/emitted to comply with IEEE-754.", version='6.6.0', ) - return _, 0 - return _, arg1 * arg2 + return _CONSTANT, 0 + return _CONSTANT, ans def _handle_product_constant_ANY(visitor, node, arg1, arg2): @@ -324,8 +322,7 @@ def _handle_division_nonlinear(visitor, node, arg1, arg2): # -def _handle_pow_constant_constant(visitor, node, *args): - arg1, arg2 = args +def _handle_pow_constant_constant(visitor, node, arg1, arg2): ans = apply_node_operation(node, (arg1[1], arg2[1])) if ans.__class__ in native_complex_types: ans = complex_number_error(ans, visitor, node) From 795bb26fda37024bef96467b484ef138ebbfaa17 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:08:17 -0700 Subject: [PATCH 0613/1178] update tests: we no longer cache the unknown error handler --- pyomo/repn/tests/test_util.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index b5e4cc4facf..e0fea0fb45c 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -718,16 +718,14 @@ class UnknownExpression(NumericExpression): DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" ): end[node.__class__](None, node, *node.args) - self.assertEqual(len(end), 9) - self.assertIn(UnknownExpression, end) + self.assertEqual(len(end), 8) node = UnknownExpression((6, 7)) with self.assertRaisesRegex( DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" ): end[node.__class__, 6, 7](None, node, *node.args) - self.assertEqual(len(end), 10) - self.assertIn((UnknownExpression, 6, 7), end) + self.assertEqual(len(end), 8) def test_BeforeChildDispatcher_registration(self): class BeforeChildDispatcherTester(BeforeChildDispatcher): From 44b7ef2fca90843d807ff818ce36efea78a09713 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 21:32:05 -0700 Subject: [PATCH 0614/1178] Fix raw string escaping --- pyomo/contrib/latex_printer/latex_printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 1d5279e984a..0a595dd8e1b 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1081,7 +1081,7 @@ def generate_set_name(st, lcm): generate_set_name(s, lcm) for s in st.subsets(False) ) else: - return str(st).replace('_', r'\_').replace('{', '\{').replace('}', '\}') + return str(st).replace('_', r'\_').replace('{', r'\{').replace('}', r'\}') # Handling the iterator indices defaultSetLatexNames = ComponentMap() From 8730e17a67541469c84a8d955ac5868c34da17a9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 21:56:42 -0700 Subject: [PATCH 0615/1178] restore unexpected_expression_type hook --- pyomo/repn/util.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 7bba4041c70..a51ee1c6d64 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -439,13 +439,16 @@ def __missing__(self, key): f"dispatcher for node '{node_class.__name__}' while walking " "expression tree." ) - raise DeveloperError( - f"Unexpected expression node type '{node_class.__name__}' " - f"found while walking expression tree in {type(self).__name__}." - ) + return self.unexpected_expression_type self[key] = fcn return fcn + def unexpected_expression_type(self, visitor, node, *args): + raise DeveloperError( + f"Unexpected expression node type '{type(node).__name__}' " + f"found while walking expression tree in {type(self).__name__}." + ) + def apply_node_operation(node, args): try: From 53fe3455429b8ee004a05d6d643cc2955f17602b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:22:09 -0700 Subject: [PATCH 0616/1178] Improve automatic flattening of LinearExpression args --- pyomo/core/expr/numeric_expr.py | 33 +++++++------ pyomo/core/expr/template_expr.py | 6 +-- .../unit/test_numeric_expr_dispatcher.py | 8 ++-- .../unit/test_numeric_expr_zerofilter.py | 8 ++-- pyomo/core/tests/unit/test_template_expr.py | 48 +++++++++---------- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index c1199ffdcad..e8f7227208c 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -2283,8 +2283,11 @@ def _iadd_mutablenpvsum_mutable(a, b): def _iadd_mutablenpvsum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2296,9 +2299,7 @@ def _iadd_mutablenpvsum_npv(a, b): def _iadd_mutablenpvsum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2379,8 +2380,11 @@ def _iadd_mutablelinear_mutable(a, b): def _iadd_mutablelinear_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2392,9 +2396,7 @@ def _iadd_mutablelinear_npv(a, b): def _iadd_mutablelinear_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2478,8 +2480,11 @@ def _iadd_mutablesum_mutable(a, b): def _iadd_mutablesum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2491,9 +2496,7 @@ def _iadd_mutablesum_npv(a, b): def _iadd_mutablesum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index f65a1f2b9b0..f982ef38d1d 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -116,7 +116,7 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0].__getitem__(tuple(args[1:])) + return args[0][*args[1:]] def _apply_operation(self, result): args = tuple( @@ -127,7 +127,7 @@ def _apply_operation(self, result): ) for arg in result[1:] ) - return result[0].__getitem__(tuple(result[1:])) + return result[0][*result[1:]] class Numeric_GetItemExpression(GetItemExpression, NumericExpression): @@ -273,7 +273,7 @@ def _to_string(self, values, verbose, smap): return "%s.%s" % (values[0], attr) def _resolve_template(self, args): - return getattr(*tuple(args)) + return getattr(*args) class Numeric_GetAttrExpression(GetAttrExpression, NumericExpression): diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3787f00de47..7c6e2af9974 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -6548,11 +6548,11 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.invalid, NotImplemented), (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, @@ -6592,7 +6592,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 162d664e0f8..34d2e1cc2c2 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -6076,11 +6076,11 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.invalid, NotImplemented), (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, @@ -6120,7 +6120,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4f255e3567a..4c872e1e11d 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -490,14 +490,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_sum_rule(self): @@ -566,14 +566,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_getattr_sum_rule(self): @@ -609,14 +609,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_eval_getattr(self): From ac6949244d4fea5ae7e4b50750a1fb3d783c3fbd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:23:03 -0700 Subject: [PATCH 0617/1178] bugfix: resolution of TemplateSumExpression --- pyomo/core/expr/template_expr.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index f982ef38d1d..6ac4c8c041f 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -19,11 +19,12 @@ from pyomo.core.expr.base import ExpressionBase, ExpressionArgs_Mixin, NPV_Mixin from pyomo.core.expr.logical_expr import BooleanExpression from pyomo.core.expr.numeric_expr import ( + ARG_TYPE, NumericExpression, - SumExpression, Numeric_NPV_Mixin, + SumExpression, + mutable_expression, register_arg_type, - ARG_TYPE, _balanced_parens, ) from pyomo.core.expr.numvalue import ( @@ -521,7 +522,15 @@ def _to_string(self, values, verbose, smap): return 'SUM(%s %s)' % (val, iterStr) def _resolve_template(self, args): - return SumExpression(args) + with mutable_expression() as e: + for arg in args: + e += arg + if e.nargs() > 1: + return e + elif not e.nargs(): + return 0 + else: + return e.arg(0) class IndexTemplate(NumericValue): From d46c90df00347927d2d56403db9c7016c0336ab3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:23:23 -0700 Subject: [PATCH 0618/1178] NFC: remove coverage pragma --- pyomo/core/expr/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/base.py b/pyomo/core/expr/base.py index f506956e478..6e2066afcc5 100644 --- a/pyomo/core/expr/base.py +++ b/pyomo/core/expr/base.py @@ -360,7 +360,7 @@ def size(self): """ return visitor.sizeof_expression(self) - def _apply_operation(self, result): # pragma: no cover + def _apply_operation(self, result): """ Compute the values of this node given the values of its children. From dd27f662ecfe56f80343736a4839cb58faeaf22a Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 17:30:17 -0700 Subject: [PATCH 0619/1178] add option to remove bounds from fixed variables to TemporarySubsystemManager --- pyomo/util/subsystems.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 70a0af1b2a7..3e4be46fca0 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -148,7 +148,14 @@ class TemporarySubsystemManager(object): """ - def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None): + def __init__( + self, + to_fix=None, + to_deactivate=None, + to_reset=None, + to_unfix=None, + remove_bounds_on_fix=False, + ): """ Arguments --------- @@ -168,6 +175,8 @@ def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None List of var data objects to be temporarily unfixed. These are restored to their original status on exit from this object's context manager. + remove_bounds_on_fix: Bool + Whether bounds should be removed temporarily for fixed variables """ if to_fix is None: @@ -194,6 +203,8 @@ def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None self._con_was_active = None self._comp_original_value = None self._var_was_unfixed = None + self._remove_bounds_on_fix = remove_bounds_on_fix + self._fixed_var_bounds = None def __enter__(self): to_fix = self._vars_to_fix @@ -203,8 +214,13 @@ def __enter__(self): self._var_was_fixed = [(var, var.fixed) for var in to_fix + to_unfix] self._con_was_active = [(con, con.active) for con in to_deactivate] self._comp_original_value = [(comp, comp.value) for comp in to_set] + self._fixed_var_bounds = [(var.lb, var.ub) for var in to_fix] for var in self._vars_to_fix: + if self._remove_bounds_on_fix: + # TODO: Potentially override var.domain as well? + var.setlb(None) + var.setub(None) var.fix() for con in self._cons_to_deactivate: @@ -223,6 +239,11 @@ def __exit__(self, ex_type, ex_val, ex_bt): var.fix() else: var.unfix() + if self._remove_bounds_on_fix: + for var, (lb, ub) in zip(self._vars_to_fix, self._fixed_var_bounds): + var.setlb(lb) + var.setub(ub) + for con, was_active in self._con_was_active: if was_active: con.activate() From 1a1c1a88966ebd2066dc6b0418f8c9b36eb0ced6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 17:30:39 -0700 Subject: [PATCH 0620/1178] timing calls and option to not use calculate_variable_from_constraint in solve_strongly_connected_components --- .../contrib/incidence_analysis/scc_solver.py | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 835e07c7c02..d965fb38203 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -11,6 +11,7 @@ import logging +from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks @@ -18,6 +19,7 @@ IncidenceGraphInterface, _generate_variables_in_constraints, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod _log = logging.getLogger(__name__) @@ -73,7 +75,13 @@ def generate_strongly_connected_components( def solve_strongly_connected_components( - block, solver=None, solve_kwds=None, calc_var_kwds=None + block, + *, + solver=None, + solve_kwds=None, + use_calc_var=False, + calc_var_kwds=None, + timer=None, ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -98,6 +106,9 @@ def solve_strongly_connected_components( a solve method. solve_kwds: Dictionary Keyword arguments for the solver's solve method + use_calc_var: Bool + Whether to use ``calculate_variable_from_constraint`` for one-by-one + square system solves calc_var_kwds: Dictionary Keyword arguments for calculate_variable_from_constraint @@ -110,25 +121,36 @@ def solve_strongly_connected_components( solve_kwds = {} if calc_var_kwds is None: calc_var_kwds = {} + if timer is None: + timer = HierarchicalTimer() + timer.start("igraph") igraph = IncidenceGraphInterface( - block, active=True, include_fixed=False, include_inequality=False + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, ) + timer.stop("igraph") constraints = igraph.constraints variables = igraph.variables res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) + timer.start("generate-scc") for scc, inputs in generate_strongly_connected_components(constraints, variables): - with TemporarySubsystemManager(to_fix=inputs): + timer.stop("generate-scc") + with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) - if N == 1: + if N == 1 and use_calc_var: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") + timer.start("calc-var-from-con") results = calculate_variable_from_constraint( scc.vars[0], scc.cons[0], **calc_var_kwds ) - res_list.append(results) + timer.stop("calc-var-from-con") else: if solver is None: var_names = [var.name for var in scc.vars.values()][:10] @@ -141,6 +163,10 @@ def solve_strongly_connected_components( ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") + timer.start("scc-subsolver") results = solver.solve(scc, **solve_kwds) - res_list.append(results) + timer.stop("scc-subsolver") + res_list.append(results) + timer.start("generate-scc") + timer.stop("generate-scc") return res_list From 3d7f2c30da36f12b3f49f5cef0f2f887fe19fec2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 17:50:17 -0700 Subject: [PATCH 0621/1178] timing calls in generate-scc and option to reuse an incidence graph if one already exists --- .../contrib/incidence_analysis/scc_solver.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index d965fb38203..1d59be0bc71 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -26,7 +26,11 @@ def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False + constraints, + variables=None, + include_fixed=False, + igraph=None, + timer=None, ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization @@ -55,23 +59,38 @@ def generate_strongly_connected_components( "input variables" for that block. """ + if timer is None: + timer = HierarchicalTimer() if variables is None: + timer.start("generate-vars") variables = list( - _generate_variables_in_constraints(constraints, include_fixed=include_fixed) + _generate_variables_in_constraints( + constraints, + include_fixed=include_fixed, + #method=IncidenceMethod.ampl_repn + ) ) + timer.stop("generate-vars") assert len(variables) == len(constraints) - igraph = IncidenceGraphInterface() + if igraph is None: + igraph = IncidenceGraphInterface() + timer.start("block-triang") var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) + timer.stop("block-triang") subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] + timer.start("subsystem-blocks") for block, inputs in generate_subsystem_blocks( subsets, include_fixed=include_fixed ): + timer.stop("subsystem-blocks") # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) + timer.start("subsystem-blocks") + timer.stop("subsystem-blocks") def solve_strongly_connected_components( @@ -139,7 +158,9 @@ def solve_strongly_connected_components( res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) timer.start("generate-scc") - for scc, inputs in generate_strongly_connected_components(constraints, variables): + for scc, inputs in generate_strongly_connected_components( + constraints, variables, timer=timer, igraph=igraph + ): timer.stop("generate-scc") with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) From 3b5aaa66d0abd83064f48762d54f759a52a3a92d Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 09:06:56 -0700 Subject: [PATCH 0622/1178] remove unnecessary import --- pyomo/contrib/incidence_analysis/scc_solver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index e7ec2bfc75b..b95a9bb66a5 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -29,7 +29,6 @@ _log = logging.getLogger(__name__) -from pyomo.common.timing import HierarchicalTimer def generate_strongly_connected_components( constraints, variables=None, From 565a29ecdcbae605f381b3e7cd13ae112843babb Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 09:08:11 -0700 Subject: [PATCH 0623/1178] remove unnecessary imports --- pyomo/contrib/incidence_analysis/scc_solver.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index b95a9bb66a5..5e21631f2ef 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -14,11 +14,7 @@ from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint -from pyomo.util.subsystems import ( - TemporarySubsystemManager, - generate_subsystem_blocks, - create_subsystem_block, -) +from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks from pyomo.contrib.incidence_analysis.interface import ( IncidenceGraphInterface, _generate_variables_in_constraints, From 1902e8e1f9ce04153a22d324fae5015342eb23ce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:11:35 -0700 Subject: [PATCH 0624/1178] Allow bare variables in LinearExpression nodes --- pyomo/core/expr/numeric_expr.py | 39 ++++++++++++++++-------------- pyomo/repn/linear.py | 38 ++++++++++++++++------------- pyomo/repn/plugins/baron_writer.py | 19 ++++++++++++--- pyomo/repn/plugins/gams_writer.py | 10 +++++++- pyomo/repn/plugins/nl_writer.py | 14 +++++++++++ pyomo/repn/quadratic.py | 25 +++++++------------ pyomo/repn/standard_repn.py | 22 +++++++++++++++++ pyomo/repn/tests/test_linear.py | 2 +- 8 files changed, 112 insertions(+), 57 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index e8f7227208c..2cf4073b49f 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1298,8 +1298,14 @@ def _build_cache(self): if arg.__class__ is MonomialTermExpression: coef.append(arg._args_[0]) var.append(arg._args_[1]) - else: + elif arg.__class__ in native_numeric_types: const += arg + elif not arg.is_potentially_variable(): + const += arg + else: + assert arg.is_potentially_variable() + coef.append(1) + var.append(arg) LinearExpression._cache = (self, const, coef, var) @property @@ -1325,7 +1331,7 @@ def create_node_with_local_data(self, args, classtype=None): classtype = self.__class__ if type(args) is not list: args = list(args) - for i, arg in enumerate(args): + for arg in args: if arg.__class__ in self._allowable_linear_expr_arg_types: # 99% of the time, the arg type hasn't changed continue @@ -1336,8 +1342,7 @@ def create_node_with_local_data(self, args, classtype=None): # NPV expressions are OK pass elif arg.is_variable_type(): - # vars are OK, but need to be mapped to monomial terms - args[i] = MonomialTermExpression((1, arg)) + # vars are OK continue else: # For anything else, convert this to a general sum @@ -1820,7 +1825,7 @@ def _add_native_param(a, b): def _add_native_var(a, b): if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_native_monomial(a, b): @@ -1871,7 +1876,7 @@ def _add_npv_param(a, b): def _add_npv_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_npv_monomial(a, b): @@ -1929,7 +1934,7 @@ def _add_param_var(a, b): a = a.value if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_param_monomial(a, b): @@ -1972,11 +1977,11 @@ def _add_param_other(a, b): def _add_var_native(a, b): if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_npv(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_param(a, b): @@ -1984,21 +1989,19 @@ def _add_var_param(a, b): b = b.value if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_var(a, b): - return LinearExpression( - [MonomialTermExpression((1, a)), MonomialTermExpression((1, b))] - ) + return LinearExpression([a, b]) def _add_var_monomial(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_linear(a, b): - return b._trunc_append(MonomialTermExpression((1, a))) + return b._trunc_append(a) def _add_var_sum(a, b): @@ -2033,7 +2036,7 @@ def _add_monomial_param(a, b): def _add_monomial_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_monomial_monomial(a, b): @@ -2076,7 +2079,7 @@ def _add_linear_param(a, b): def _add_linear_var(a, b): - return a._trunc_append(MonomialTermExpression((1, b))) + return a._trunc_append(b) def _add_linear_monomial(a, b): @@ -2403,7 +2406,7 @@ def _iadd_mutablelinear_param(a, b): def _iadd_mutablelinear_var(a, b): - a._args_.append(MonomialTermExpression((1, b))) + a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 6ab4abfdaf5..d601ccbcd7c 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -31,8 +31,8 @@ MonomialTermExpression, LinearExpression, SumExpression, - NPV_SumExpression, ExternalFunctionExpression, + mutable_expression, ) from pyomo.core.expr.relational_expr import ( EqualityExpression, @@ -120,22 +120,14 @@ def to_expression(self, visitor): ans = 0 if self.linear: var_map = visitor.var_map - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: @@ -704,6 +696,18 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_numeric_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + const += visitor.check_constant(arg.value, arg) + continue + LinearBeforeChildDispatcher._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index de19b5aad73..ab673b0c1c3 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -174,15 +174,26 @@ def _monomial_to_string(self, node): return self.smap.getSymbol(var) return ftoa(const, True) + '*' + self.smap.getSymbol(var) + def _var_to_string(self, node): + if node.is_fixed(): + return ftoa(node.value, True) + self.variables.add(id(node)) + return self.smap.getSymbol(node) + def _linear_to_string(self, node): values = [ ( self._monomial_to_string(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + if arg.__class__ is EXPR.MonomialTermExpression + else ( + ftoa(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_string(arg) + if arg.is_variable_type() + else ftoa(value(arg), True) + ) ) - else ftoa(value(arg)) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 5f94f176762..0756cb64920 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -183,7 +183,15 @@ def _linear_to_string(self, node): ( self._monomial_to_string(arg) if arg.__class__ is EXPR.MonomialTermExpression - else ftoa(arg, True) + else ( + ftoa(arg, True) + if arg.__class__ in native_numeric_types + else ( + self.smap.getSymbol(arg) + if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + else ftoa(value(arg), True) + ) + ) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a256cd1b900..b82d4df77e2 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2780,6 +2780,20 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg) + const += visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index c538d1efc7f..0ddfda829ed 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -98,22 +98,15 @@ def to_expression(self, visitor): e += coef * (var_map[x1] * var_map[x2]) ans += e if self.linear: - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + var_map = visitor.var_map + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 8700872f04f..8600a8a50f6 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -321,6 +321,16 @@ def generate_standard_repn( linear_vars[id_] = v elif arg.__class__ in native_numeric_types: C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg.value + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += EXPR.evaluate_expression(arg) else: # compute_values == False @@ -336,6 +346,18 @@ def generate_standard_repn( else: linear_coefs[id_] = c linear_vars[id_] = v + elif arg.__class__ in native_numeric_types: + C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += arg diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 6843650d0c2..0fd428fd8ee 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1589,7 +1589,7 @@ def test_to_expression(self): expr.constant = 0 expr.linear[id(m.x)] = 0 expr.linear[id(m.y)] = 0 - assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression()) + assertExpressionsEqual(self, expr.to_expression(visitor), 0) @unittest.skipUnless(numpy_available, "Test requires numpy") def test_nonnumeric(self): From f36e31109b878178eea70c88d496ece1fd864316 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:34:52 -0700 Subject: [PATCH 0625/1178] NFC: apply black --- pyomo/repn/plugins/gams_writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 0756cb64920..a0f407d7952 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -188,7 +188,8 @@ def _linear_to_string(self, node): if arg.__class__ in native_numeric_types else ( self.smap.getSymbol(arg) - if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + if arg.is_variable_type() + and (not arg.fixed or self.output_fixed_variables) else ftoa(value(arg), True) ) ) From df4f7af6d75e1a29f3a23c8144bb0945c8908f32 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 10:03:45 -0700 Subject: [PATCH 0626/1178] pass timer to generate_subsystem_blocks --- pyomo/contrib/incidence_analysis/scc_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 5e21631f2ef..117554c52de 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -84,7 +84,7 @@ def generate_strongly_connected_components( subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] timer.start("generate-block") for block, inputs in generate_subsystem_blocks( - subsets, include_fixed=include_fixed + subsets, include_fixed=include_fixed, timer=timer ): timer.stop("generate-block") # TODO: How does len scale for reference-to-list? From f47345cdaea937cd962a1db6f2ea6f5257437026 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 10:04:45 -0700 Subject: [PATCH 0627/1178] accept timer argument in generate_subsystem_blocks, revert implementation of identify_external_functions to be a generator --- pyomo/util/subsystems.py | 53 +++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index ba1d56a787b..9a2a8b6635d 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -17,6 +17,7 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.expression import Expression +from pyomo.core.base.objective import Objective from pyomo.core.base.external import ExternalFunction from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.core.expr.numeric_expr import ExternalFunctionExpression @@ -67,47 +68,37 @@ def acceptChildResult(self, node, data, child_result, child_idx): return child_result.is_expression_type(), None -def identify_external_functions( - expr, - descend_into_named_expressions=True, - named_expressions=None, -): - visitor = _ExternalFunctionVisitor( - descend_into_named_expressions=descend_into_named_expressions - ) - efs = list(visitor.walk_expression(expr)) - if not descend_into_named_expressions and named_expressions is not None: - named_expressions.extend(visitor.named_expressions) - return efs - #yield from _ExternalFunctionVisitor().walk_expression(expr) +def identify_external_functions(expr): + # TODO: Potentially support descend_into_named_expressions argument here. + # This will likely require converting from a generator to a function. + yield from _ExternalFunctionVisitor().walk_expression(expr) def add_local_external_functions(block): ef_exprs = [] named_expressions = [] - for comp in block.component_data_objects((Constraint, Expression), active=True): - ef_exprs.extend(identify_external_functions( - comp.expr, - descend_into_named_expressions=False, - named_expressions=named_expressions, - )) - named_expr_set = ComponentSet(named_expressions) + visitor = _ExternalFunctionVisitor(descend_into_named_expressions=False) + for comp in block.component_data_objects( + (Constraint, Expression, Objective), + active=True, + ): + ef_exprs.extend(visitor.walk_expression(comp.expr)) + named_expr_set = ComponentSet(visitor.named_expressions) + # List of unique named expressions named_expressions = list(named_expr_set) while named_expressions: expr = named_expressions.pop() - local_named_exprs = [] - ef_exprs.extend(identify_external_functions( - expr, - descend_into_named_expressions=False, - named_expressions=local_named_exprs, - )) + # Clear named expression cache so we don't re-check named expressions + # we've seen before. + visitor.named_expressions.clear() + ef_exprs.extend(visitor.walk_expression(expr)) # Only add to the stack named expressions that we have # not encountered yet. - for local_expr in local_named_exprs: + for local_expr in visitor.named_expressions: if local_expr not in named_expr_set: named_expressions.append(local_expr) named_expr_set.add(local_expr) - + unique_functions = [] fcn_set = set() for expr in ef_exprs: @@ -184,7 +175,7 @@ def create_subsystem_block( return block -def generate_subsystem_blocks(subsystems, include_fixed=False): +def generate_subsystem_blocks(subsystems, include_fixed=False, timer=None): """Generates blocks that contain subsystems of variables and constraints. Arguments @@ -203,8 +194,10 @@ def generate_subsystem_blocks(subsystems, include_fixed=False): not specified are contained in the input_vars component. """ + if timer is None: + timer = HierarchicalTimer() for cons, vars in subsystems: - block = create_subsystem_block(cons, vars, include_fixed) + block = create_subsystem_block(cons, vars, include_fixed, timer=timer) yield block, list(block.input_vars.values()) From 9eebe2297961854783dd5feb57595b46d8aa3866 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:41:49 -0700 Subject: [PATCH 0628/1178] This is a small bug fix to address when there is no objective --- pyomo/contrib/solver/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 13bd5ddb212..a2174fea237 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -22,6 +22,7 @@ from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning +from pyomo.opt import ProblemSense from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -418,9 +419,15 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) + legacy_results.problem.number_of_constraints = model.nconstraints() + legacy_results.problem.number_of_variables = model.nvariables() obj = get_objective(model) - if len(list(obj)) > 0: + if not obj: + legacy_results.problem.sense = ProblemSense.unknown + legacy_results.problem.number_of_objectives = 0 + else: legacy_results.problem.sense = obj.sense + legacy_results.problem.number_of_objectives = len(obj) if obj.sense == minimize: legacy_results.problem.lower_bound = results.objective_bound From 28ecd960b60a2691a43c7eea688252046253e84a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:45:42 -0700 Subject: [PATCH 0629/1178] Cannot convert non-constant Pyomo to bool --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index a2174fea237..035d25bf97d 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -422,7 +422,7 @@ def _map_results(self, model, results): legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() obj = get_objective(model) - if not obj: + if len(obj) == 0: legacy_results.problem.sense = ProblemSense.unknown legacy_results.problem.number_of_objectives = 0 else: From 191fadf94dd9ff1ea5e41ad3d15e193f0346d861 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:55:55 -0700 Subject: [PATCH 0630/1178] Change way of checking number of objectives --- pyomo/contrib/solver/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 035d25bf97d..91a581c5998 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -421,11 +421,11 @@ def _map_results(self, model, results): legacy_results.solver.termination_message = str(results.termination_condition) legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() - obj = get_objective(model) - if len(obj) == 0: + if model.nobjectives() == 0: legacy_results.problem.sense = ProblemSense.unknown legacy_results.problem.number_of_objectives = 0 else: + obj = get_objective(model) legacy_results.problem.sense = obj.sense legacy_results.problem.number_of_objectives = len(obj) From 2167deaa97de640e872d4004c979c08d21020ba1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:57:44 -0700 Subject: [PATCH 0631/1178] Problem sense is already unknown by default --- pyomo/contrib/solver/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 91a581c5998..f9cd213bf73 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -422,7 +422,6 @@ def _map_results(self, model, results): legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() if model.nobjectives() == 0: - legacy_results.problem.sense = ProblemSense.unknown legacy_results.problem.number_of_objectives = 0 else: obj = get_objective(model) From ff2ddad1f91713071221c9eaf003869b939d379f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:58:22 -0700 Subject: [PATCH 0632/1178] Remove import --- pyomo/contrib/solver/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index f9cd213bf73..54871e90c2f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -22,7 +22,6 @@ from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning -from pyomo.opt import ProblemSense from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize From db4062419b56d810f30c05a96f72cca5eefdfd1a Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 11:07:40 -0700 Subject: [PATCH 0633/1178] add failing test --- .../solvers/tests/test_persistent_solvers.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index af615d1ed8b..ae189aca701 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -918,6 +918,27 @@ def test_bounds_with_params( res = opt.solve(m) self.assertAlmostEqual(m.y.value, 3) + @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) + def test_bounds_with_immutable_params( + self, name: str, opt_class: Type[PersistentSolver], only_child_vars + ): + # this test is for issue #2574 + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.p = pe.Param(mutable=False, initialize=1) + m.q = pe.Param([1, 2], mutable=False, initialize=10) + m.y = pe.Var() + m.y.setlb(m.p) + m.y.setub(m.q[1]) + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.y.setlb(m.q[2]) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 10) + @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( self, name: str, opt_class: Type[PersistentSolver], only_child_vars From b09b3077c10be1431452c877e86593476f267a1b Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 11:10:05 -0700 Subject: [PATCH 0634/1178] apply patch --- pyomo/contrib/appsi/cmodel/src/expression.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index 234ef47e86f..8079de42b21 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1548,7 +1548,10 @@ appsi_operator_from_pyomo_expr(py::handle expr, py::handle var_map, break; } case param: { - res = param_map[expr_types.id(expr)].cast>(); + if (expr.attr("parent_component")().attr("mutable").cast()) + res = param_map[expr_types.id(expr)].cast>(); + else + res = std::make_shared(expr.attr("value").cast()); break; } case product: { From 9bbb8871d7407574bf22d25e52f4553d3f9da53e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 11:23:25 -0700 Subject: [PATCH 0635/1178] Standardize subprocess_timeout import to 2; move to a central location --- pyomo/contrib/appsi/solvers/ipopt.py | 3 ++- pyomo/contrib/solver/ipopt.py | 6 +++--- pyomo/opt/base/__init__.py | 2 ++ pyomo/solvers/plugins/solvers/CONOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/CPLEX.py | 10 ++++++++-- pyomo/solvers/plugins/solvers/GLPK.py | 3 ++- pyomo/solvers/plugins/solvers/IPOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 4 ++-- 8 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 29e74f81c98..82f851ce02c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -42,6 +42,7 @@ import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -158,7 +159,7 @@ def available(self): def version(self): results = subprocess.run( [str(self.config.executable), '--version'], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index dc632adb184..8c5e13a534e 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging import os import subprocess import datetime @@ -38,8 +39,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix from pyomo.common.collections import ComponentMap - -import logging +from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -229,7 +229,7 @@ def version(self, config=None): else: results = subprocess.run( [str(pth), '--version'], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index 8d11114dd09..c625c09d1c0 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -22,3 +22,5 @@ from pyomo.opt.base.results import ReaderFactory, AbstractResultsReader from pyomo.opt.base.problem import AbstractProblemWriter, BranchDirection, WriterFactory from pyomo.opt.base.formats import ProblemFormat, ResultsFormat, guess_format + +subprocess_timeout = 2 diff --git a/pyomo/solvers/plugins/solvers/CONOPT.py b/pyomo/solvers/plugins/solvers/CONOPT.py index 89ee3848805..bde68d32c55 100644 --- a/pyomo/solvers/plugins/solvers/CONOPT.py +++ b/pyomo/solvers/plugins/solvers/CONOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat +from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index b2b8c5e988d..f7a4774b073 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -21,7 +21,13 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections import ComponentMap, Bunch -from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver, BranchDirection +from pyomo.opt.base import ( + ProblemFormat, + ResultsFormat, + OptSolver, + BranchDirection, + subprocess_timeout, +) from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverResults, @@ -404,7 +410,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, '-c', 'quit'], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index 39948d465f4..2e09aae1668 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -29,6 +29,7 @@ SolutionStatus, ProblemSense, ) +from pyomo.opt.base import subprocess_timeout from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver from pyomo.solvers.mockmip import MockMIP @@ -137,7 +138,7 @@ def _get_version(self, executable=None): [executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - timeout=1, + timeout=subprocess_timeout, universal_newlines=True, ) return _extract_version(result.stdout) diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index deda4314a52..84017a7596e 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat +from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, "-v"], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index be7415a19ef..50191d82e5e 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -18,7 +18,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat +from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverStatus, @@ -103,7 +103,7 @@ def _get_version(self, solver_exec=None): return _extract_version('') results = subprocess.run( [solver_exec, "--version"], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, From 0b252aecfade0cd0e3327abef24cfb8e061ba164 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 11:23:41 -0700 Subject: [PATCH 0636/1178] add tests with external functions in named expressions --- pyomo/util/tests/test_subsystems.py | 51 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index 87a4fb3cf28..f102670ba62 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -292,7 +292,7 @@ def test_generate_dont_fix_inputs_with_fixed_var(self): self.assertFalse(m.v3.fixed) self.assertTrue(m.v4.fixed) - def _make_model_with_external_functions(self): + def _make_model_with_external_functions(self, named_expressions=False): m = pyo.ConcreteModel() gsl = find_GSL() m.bessel = pyo.ExternalFunction(library=gsl, function="gsl_sf_bessel_J0") @@ -300,9 +300,18 @@ def _make_model_with_external_functions(self): m.v1 = pyo.Var(initialize=1.0) m.v2 = pyo.Var(initialize=2.0) m.v3 = pyo.Var(initialize=3.0) + if named_expressions: + m.subexpr = pyo.Expression(pyo.PositiveIntegers) + subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) + subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) + subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3 ** 2 + else: + subexpr1 = 2 * m.fermi(m.v1) + subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) + subexpr3 = m.subexpr[2] + m.v3 ** 2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) - m.con2 = pyo.Constraint(expr=2 * m.fermi(m.v1) + m.v2**2 - m.v3 == 1.0) - m.con3 = pyo.Constraint(expr=m.bessel(m.v1) - m.bessel(m.v2) + m.v3**2 == 2.0) + m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) + m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) return m @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") @@ -329,6 +338,15 @@ def test_identify_external_functions(self): pred_fcn_data = {(gsl, "gsl_sf_bessel_J0"), (gsl, "gsl_sf_fermi_dirac_m1")} self.assertEqual(fcn_data, pred_fcn_data) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") + def test_local_external_functions_with_named_expressions(self): + m = self._make_model_with_external_functions(named_expressions=True) + variables = list(pyo.component_data_objects(pyo.Var)) + constraints = list(pyo.component_data_objects(pyo.Constraint, active=True)) + b = create_subsystem_block(constraints, variables) + self.assertTrue(isinstance(m._gsl_sf_bessel_J0, pyo.ExternalFunction)) + self.assertTrue(isinstance(m._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) + def _solve_ef_model_with_ipopt(self): m = self._make_model_with_external_functions() ipopt = pyo.SolverFactory("ipopt") @@ -362,6 +380,33 @@ def test_with_external_function(self): self.assertAlmostEqual(m.v2.value, m_full.v2.value) self.assertAlmostEqual(m.v3.value, m_full.v3.value) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(), "ipopt is not available" + ) + def test_with_external_function_in_named_expression(self): + m = self._make_model_with_external_functions(named_expressions=True) + subsystem = ([m.con2, m.con3], [m.v2, m.v3]) + + m.v1.set_value(0.5) + block = create_subsystem_block(*subsystem) + ipopt = pyo.SolverFactory("ipopt") + with TemporarySubsystemManager(to_fix=list(block.input_vars.values())): + ipopt.solve(block) + + # Correct values obtained by solving with Ipopt directly + # in another script. + self.assertEqual(m.v1.value, 0.5) + self.assertFalse(m.v1.fixed) + self.assertAlmostEqual(m.v2.value, 1.04816, delta=1e-5) + self.assertAlmostEqual(m.v3.value, 1.34356, delta=1e-5) + + # Result obtained by solving the full system + m_full = self._solve_ef_model_with_ipopt() + self.assertAlmostEqual(m.v1.value, m_full.v1.value) + self.assertAlmostEqual(m.v2.value, m_full.v2.value) + self.assertAlmostEqual(m.v3.value, m_full.v3.value) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") def test_external_function_with_potential_name_collision(self): m = self._make_model_with_external_functions() From 912867918bdda9ac48642a0e24a51cc45489bcff Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 11:30:22 -0700 Subject: [PATCH 0637/1178] document igraph option in generate_scc --- pyomo/contrib/incidence_analysis/scc_solver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 117554c52de..6c556646a8c 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -47,9 +47,12 @@ def generate_strongly_connected_components( variables: List of Pyomo variable data objects Variables that may participate in strongly connected components. If not provided, all variables in the constraints will be used. - include_fixed: Bool + include_fixed: Bool, optional Indicates whether fixed variables will be included when identifying variables in constraints. + igraph: IncidenceGraphInterface, optional + Incidence graph containing (at least) the provided constraints + and variables. Yields ------ @@ -67,7 +70,7 @@ def generate_strongly_connected_components( _generate_variables_in_constraints( constraints, include_fixed=include_fixed, - #method=IncidenceMethod.ampl_repn + method=IncidenceMethod.ampl_repn, ) ) timer.stop("generate-vars") From 78431b71f895aa87c875d811b5e05cd933ba4f8a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 12:01:07 -0700 Subject: [PATCH 0638/1178] Change implementation: make private-esque attribute that user can alter --- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- pyomo/contrib/solver/ipopt.py | 4 ++-- pyomo/opt/base/__init__.py | 2 -- pyomo/opt/solver/shellcmd.py | 1 + pyomo/solvers/plugins/solvers/CONOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/CPLEX.py | 10 ++-------- pyomo/solvers/plugins/solvers/GLPK.py | 4 ++-- pyomo/solvers/plugins/solvers/IPOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 4 ++-- 9 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 82f851ce02c..54e21d333e5 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -42,7 +42,6 @@ import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -148,6 +147,7 @@ def __init__(self, only_child_vars=False): self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None + self._version_timeout = 2 def available(self): if self.config.executable.path() is None: @@ -159,7 +159,7 @@ def available(self): def version(self): results = subprocess.run( [str(self.config.executable), '--version'], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 8c5e13a534e..edc5799ae20 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -39,7 +39,6 @@ from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix from pyomo.common.collections import ComponentMap -from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -207,6 +206,7 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None + self._version_timeout = 2 def available(self, config=None): if config is None: @@ -229,7 +229,7 @@ def version(self, config=None): else: results = subprocess.run( [str(pth), '--version'], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index c625c09d1c0..8d11114dd09 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -22,5 +22,3 @@ from pyomo.opt.base.results import ReaderFactory, AbstractResultsReader from pyomo.opt.base.problem import AbstractProblemWriter, BranchDirection, WriterFactory from pyomo.opt.base.formats import ProblemFormat, ResultsFormat, guess_format - -subprocess_timeout = 2 diff --git a/pyomo/opt/solver/shellcmd.py b/pyomo/opt/solver/shellcmd.py index 94117779237..baa0369e1d6 100644 --- a/pyomo/opt/solver/shellcmd.py +++ b/pyomo/opt/solver/shellcmd.py @@ -60,6 +60,7 @@ def __init__(self, **kwargs): # a solver plugin may not report execution time. self._last_solve_time = None self._define_signal_handlers = None + self._version_timeout = 2 if executable is not None: self.set_executable(name=executable, validate=validate) diff --git a/pyomo/solvers/plugins/solvers/CONOPT.py b/pyomo/solvers/plugins/solvers/CONOPT.py index bde68d32c55..3455eede67b 100644 --- a/pyomo/solvers/plugins/solvers/CONOPT.py +++ b/pyomo/solvers/plugins/solvers/CONOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout +from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index f7a4774b073..9f876b2d0f8 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -21,13 +21,7 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections import ComponentMap, Bunch -from pyomo.opt.base import ( - ProblemFormat, - ResultsFormat, - OptSolver, - BranchDirection, - subprocess_timeout, -) +from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver, BranchDirection from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverResults, @@ -410,7 +404,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, '-c', 'quit'], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index 2e09aae1668..e6d8576489d 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -19,6 +19,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.errors import ApplicationError from pyomo.opt import ( SolverFactory, OptSolver, @@ -29,7 +30,6 @@ SolutionStatus, ProblemSense, ) -from pyomo.opt.base import subprocess_timeout from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver from pyomo.solvers.mockmip import MockMIP @@ -138,7 +138,7 @@ def _get_version(self, executable=None): [executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - timeout=subprocess_timeout, + timeout=self._version_timeout, universal_newlines=True, ) return _extract_version(result.stdout) diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 84017a7596e..4ebbbc07d3b 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout +from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, "-v"], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index 50191d82e5e..fd69954b428 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -18,7 +18,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout +from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverStatus, @@ -103,7 +103,7 @@ def _get_version(self, solver_exec=None): return _extract_version('') results = subprocess.run( [solver_exec, "--version"], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, From 313a31019b6de36c20cb0bac66ff0340c5fa6e36 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 12:13:05 -0700 Subject: [PATCH 0639/1178] initial implementation of variable visitor that can exploit named expressions --- pyomo/core/expr/visitor.py | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 6a9b7955281..51864044396 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1388,6 +1388,85 @@ def visit(self, node): return node +class _StreamVariableVisitor(StreamBasedExpressionVisitor): + def __init__( + self, + include_fixed=False, + descend_into_named_expressions=True, + ): + self._include_fixed = include_fixed + self._descend_into_named_expressions = descend_into_named_expressions + self.named_expressions = [] + # Should we allow re-use of this visitor for multiple expressions? + + def initializeWalker(self, expr): + self._variables = [] + self._seen = set() + return True, None + + def beforeChild(self, parent, child, index): + if ( + not self._descend_into_named_expressions + and isinstance(child, NumericValue) + and child.is_named_expression_type() + ): + self.named_expressions.append(child) + return False, None + else: + return True, None + + def exitNode(self, node, data): + if node.is_variable_type() and (self._include_fixed or not node.fixed): + if id(node) not in self._seen: + self._seen.add(id(node)) + self._variables.append(node) + + def finalizeResult(self, result): + return self._variables + + def enterNode(self, node): + pass + + def acceptChildResult(self, node, data, child_result, child_idx): + if child_result.__class__ in native_types: + return False, None + return child_result.is_expression_type(), None + + +def identify_variables_in_components(components, include_fixed=True): + visitor = _StreamVariableVisitor( + include_fixed=include_fixed, descend_into_named_expressions=False + ) + all_variables = [] + for comp in components: + all_variables.extend(visitor.walk_expressions(comp.expr)) + + named_expr_set = set() + unique_named_exprs = [] + for expr in visitor.named_expressions: + if id(expr) in named_expr_set: + named_expr_set.add(id(expr)) + unique_named_exprs.append(expr) + + while unique_named_exprs: + expr = unique_named_exprs.pop() + visitor.named_expressions.clear() + all_variables.extend(visitor.walk_expression(expr.expr)) + + for new_expr in visitor.named_expressions: + if id(new_expr) not in named_expr_set: + named_expr_set.add(new_expr) + unique_named_exprs.append(new_expr) + + unique_vars = [] + var_set = set() + for var in all_variables: + if id(var) not in var_set: + var_set.add(id(var)) + unique_vars.append(var) + return unique_vars + + def identify_variables(expr, include_fixed=True): """ A generator that yields a sequence of variables From 07f5234575aeaf6905827fd7d89842e80200cdbd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 12:58:56 -0700 Subject: [PATCH 0640/1178] Update tests to track change in LinearExpression arg types --- pyomo/core/tests/transform/test_add_slacks.py | 56 +-- pyomo/core/tests/unit/test_compare.py | 6 - pyomo/core/tests/unit/test_expression.py | 11 +- pyomo/core/tests/unit/test_numeric_expr.py | 329 ++++-------------- .../core/tests/unit/test_numeric_expr_api.py | 11 +- .../unit/test_numeric_expr_dispatcher.py | 278 ++++++--------- .../unit/test_numeric_expr_zerofilter.py | 274 ++++++--------- pyomo/core/tests/unit/test_visitor.py | 23 +- pyomo/gdp/tests/common_tests.py | 7 +- pyomo/gdp/tests/test_bigm.py | 5 +- pyomo/gdp/tests/test_binary_multiplication.py | 5 +- pyomo/gdp/tests/test_disjunct.py | 24 +- 12 files changed, 302 insertions(+), 727 deletions(-) diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index 7896cab7e88..a74a9b75c4f 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -102,10 +102,7 @@ def checkRule1(self, m): self, cons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1)), - ] + [m.x, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1))] ), ) @@ -118,14 +115,7 @@ def checkRule3(self, m): self.assertEqual(cons.lower, 0.1) assertExpressionsEqual( - self, - cons.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] - ), + self, cons.body, EXPR.LinearExpression([m.x, transBlock._slack_plus_rule3]) ) def test_ub_constraint_modified(self): @@ -154,8 +144,8 @@ def test_both_bounds_constraint_modified(self): cons.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), + m.y, + transBlock._slack_plus_rule2, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule2)), ] ), @@ -184,10 +174,10 @@ def test_new_obj_created(self): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), + transBlock._slack_minus_rule1, + transBlock._slack_plus_rule2, + transBlock._slack_minus_rule2, + transBlock._slack_plus_rule3, ] ), ) @@ -302,10 +292,7 @@ def checkTargetsObj(self, m): self, obj.expr, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] + [transBlock._slack_minus_rule1, transBlock._slack_plus_rule3] ), ) @@ -423,9 +410,9 @@ def test_transformed_constraints_sumexpression_body(self): c.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.x)), + m.x, EXPR.MonomialTermExpression((-2, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule4)), + transBlock._slack_plus_rule4, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule4)), ] ), @@ -518,15 +505,9 @@ def checkTargetObj(self, m): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[1]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[2]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[3]")) - ), + transBlock.component("_slack_plus_rule1[1]"), + transBlock.component("_slack_plus_rule1[2]"), + transBlock.component("_slack_plus_rule1[3]"), ] ), ) @@ -558,14 +539,7 @@ def checkTransformedRule1(self, m, i): EXPR.LinearExpression( [ EXPR.MonomialTermExpression((2, m.x[i])), - EXPR.MonomialTermExpression( - ( - 1, - m._core_add_slack_variables.component( - "_slack_plus_rule1[%s]" % i - ), - ) - ), + m._core_add_slack_variables.component("_slack_plus_rule1[%s]" % i), ] ), ) diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py index f80753bdb61..7c3536bc084 100644 --- a/pyomo/core/tests/unit/test_compare.py +++ b/pyomo/core/tests/unit/test_compare.py @@ -165,17 +165,11 @@ def test_expr_if(self): 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, - (MonomialTermExpression, 2), - 1, m.x, 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, (MonomialTermExpression, 2), -1, diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index c9afc6a1f76..678df4c01a8 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -738,10 +738,10 @@ def test_pprint_oldStyle(self): expr = model.e * model.x**2 + model.E[1] output = """\ -sum(prod(e{sum(mon(1, x), 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) +sum(prod(e{sum(x, 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) e : Size=1, Index=None Key : Expression - None : sum(mon(1, x), 2) + None : sum(x, 2) E : Size=2, Index={1, 2} Key : Expression 1 : sum(pow(x, 2), 1) @@ -951,12 +951,7 @@ def test_isub(self): assertExpressionsEqual( self, m.e.expr, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, m.y)), - ] - ), + EXPR.LinearExpression([m.x, EXPR.MonomialTermExpression((-1, m.y))]), ) self.assertTrue(compare_expressions(m.e.expr, m.x - m.y)) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index c1066c292d7..968b3acb6a4 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -638,12 +638,7 @@ def test_simpleSum(self): m.b = Var() e = m.a + m.b # - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b])) self.assertRaises(KeyError, e.arg, 3) @@ -654,14 +649,7 @@ def test_simpleSum_API(self): e = m.a + m.b e += 2 * m.a self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((2, m.a)), - ] - ), + e, LinearExpression([m.a, m.b, MonomialTermExpression((2, m.a))]) ) def test_constSum(self): @@ -669,13 +657,9 @@ def test_constSum(self): m = AbstractModel() m.a = Var() # - self.assertExpressionsEqual( - m.a + 5, LinearExpression([MonomialTermExpression((1, m.a)), 5]) - ) + self.assertExpressionsEqual(m.a + 5, LinearExpression([m.a, 5])) - self.assertExpressionsEqual( - 5 + m.a, LinearExpression([5, MonomialTermExpression((1, m.a))]) - ) + self.assertExpressionsEqual(5 + m.a, LinearExpression([5, m.a])) def test_nestedSum(self): # @@ -696,12 +680,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + 5 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -710,12 +689,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = 5 + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -724,16 +698,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + m.c - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -742,16 +707,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = m.c + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -762,17 +718,7 @@ def test_nestedSum(self): e2 = m.c + m.d e = e1 + e2 # - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c, m.d])) def test_nestedSum2(self): # @@ -798,22 +744,7 @@ def test_nestedSum2(self): self.assertExpressionsEqual( e, - SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] - ), + SumExpression([ProductExpression((2, LinearExpression([m.a, m.b]))), m.c]), ) # * @@ -834,20 +765,7 @@ def test_nestedSum2(self): ( 3, SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] + [ProductExpression((2, LinearExpression([m.a, m.b]))), m.c] ), ) ), @@ -891,10 +809,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + m.b # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((5, m.a)), MonomialTermExpression((1, m.b))] - ), + e, LinearExpression([MonomialTermExpression((5, m.a)), m.b]) ) # + @@ -905,10 +820,7 @@ def test_sumOf_nestedTrivialProduct(self): e = m.b + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.b)), MonomialTermExpression((5, m.a))] - ), + e, LinearExpression([m.b, MonomialTermExpression((5, m.a))]) ) # + @@ -920,14 +832,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + e2 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) # + @@ -939,14 +844,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e2 + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) def test_simpleDiff(self): @@ -962,10 +860,7 @@ def test_simpleDiff(self): # a b e = m.a - m.b self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((-1, m.b))] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b))]) ) def test_constDiff(self): @@ -978,9 +873,7 @@ def test_constDiff(self): # - # / \ # a 5 - self.assertExpressionsEqual( - m.a - 5, LinearExpression([MonomialTermExpression((1, m.a)), -5]) - ) + self.assertExpressionsEqual(m.a - 5, LinearExpression([m.a, -5])) # - # / \ @@ -1002,10 +895,7 @@ def test_paramDiff(self): # a p e = m.a - m.p self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), NPV_NegationExpression((m.p,))] - ), + e, LinearExpression([m.a, NPV_NegationExpression((m.p,))]) ) # - @@ -1079,14 +969,7 @@ def test_nestedDiff(self): e1 = m.a - m.b e = e1 - 5 self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - -5, - ] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b)), -5]) ) # - @@ -1102,14 +985,7 @@ def test_nestedDiff(self): [ 5, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1126,7 +1002,7 @@ def test_nestedDiff(self): e, LinearExpression( [ - MonomialTermExpression((1, m.a)), + m.a, MonomialTermExpression((-1, m.b)), MonomialTermExpression((-1, m.c)), ] @@ -1146,14 +1022,7 @@ def test_nestedDiff(self): [ m.c, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1171,21 +1040,9 @@ def test_nestedDiff(self): e, SumExpression( [ - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), + LinearExpression([m.a, MonomialTermExpression((-1, m.b))]), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.c)), - MonomialTermExpression((-1, m.d)), - ] - ), - ) + (LinearExpression([m.c, MonomialTermExpression((-1, m.d))]),) ), ] ), @@ -1382,10 +1239,7 @@ def test_sumOf_nestedTrivialProduct2(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), - ] + [m.b, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a))] ), ) @@ -1403,14 +1257,7 @@ def test_sumOf_nestedTrivialProduct2(self): [ MonomialTermExpression((m.p, m.a)), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((-1, m.c)), - ] - ), - ) + (LinearExpression([m.b, MonomialTermExpression((-1, m.c))]),) ), ] ), @@ -1428,7 +1275,7 @@ def test_sumOf_nestedTrivialProduct2(self): e, LinearExpression( [ - MonomialTermExpression((1, m.b)), + m.b, MonomialTermExpression((-1, m.c)), MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), ] @@ -1598,22 +1445,7 @@ def test_nestedProduct2(self): self.assertExpressionsEqual( e, ProductExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + (LinearExpression([m.a, m.b, m.c]), LinearExpression([m.a, m.b, m.d])) ), ) # Verify shared args... @@ -1638,9 +1470,7 @@ def test_nestedProduct2(self): e3 = e1 * m.d e = e2 * e3 # - inner = LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ) + inner = LinearExpression([m.a, m.b]) self.assertExpressionsEqual( e, ProductExpression( @@ -2034,10 +1864,10 @@ def test_sum(self): model.p = Param(mutable=True) expr = 5 + model.a + model.a - self.assertEqual("sum(5, mon(1, a), mon(1, a))", str(expr)) + self.assertEqual("sum(5, a, a)", str(expr)) expr += 5 - self.assertEqual("sum(5, mon(1, a), mon(1, a), 5)", str(expr)) + self.assertEqual("sum(5, a, a, 5)", str(expr)) expr = 2 + model.p self.assertEqual("sum(2, p)", str(expr)) @@ -2053,24 +1883,18 @@ def test_linearsum(self): expr = quicksum(i * model.a[i] for i in A) self.assertEqual( - "sum(mon(0, a[0]), mon(1, a[1]), mon(2, a[2]), mon(3, a[3]), " - "mon(4, a[4]))", + "sum(mon(0, a[0]), a[1], mon(2, a[2]), mon(3, a[3]), " "mon(4, a[4]))", str(expr), ) expr = quicksum((i - 2) * model.a[i] for i in A) self.assertEqual( - "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), mon(1, a[3]), " - "mon(2, a[4]))", + "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), a[3], " "mon(2, a[4]))", str(expr), ) expr = quicksum(model.a[i] for i in A) - self.assertEqual( - "sum(mon(1, a[0]), mon(1, a[1]), mon(1, a[2]), mon(1, a[3]), " - "mon(1, a[4]))", - str(expr), - ) + self.assertEqual("sum(a[0], a[1], a[2], a[3], a[4])", str(expr)) model.p[1].value = 0 model.p[3].value = 3 @@ -2138,10 +1962,10 @@ def test_inequality(self): self.assertEqual("5 <= a < 10", str(expr)) expr = 5 <= model.a + 5 - self.assertEqual("5 <= sum(mon(1, a), 5)", str(expr)) + self.assertEqual("5 <= sum(a, 5)", str(expr)) expr = expr < 10 - self.assertEqual("5 <= sum(mon(1, a), 5) < 10", str(expr)) + self.assertEqual("5 <= sum(a, 5) < 10", str(expr)) def test_equality(self): # @@ -2166,10 +1990,10 @@ def test_equality(self): self.assertEqual("a == 10", str(expr)) expr = 5 == model.a + 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) expr = model.a + 5 == 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) def test_getitem(self): m = ConcreteModel() @@ -2206,7 +2030,7 @@ def test_small_expression(self): expr = abs(expr) self.assertEqual( "abs(neg(pow(2, div(2, prod(2, sum(1, neg(pow(div(prod(sum(" - "mon(1, a), 1, -1), a), a), b)), 1))))))", + "a, 1, -1), a), a), b)), 1))))))", str(expr), ) @@ -3754,13 +3578,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3872,16 +3690,16 @@ def test_summation_compression(self): e, LinearExpression( [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - MonomialTermExpression((1, self.m.b[1])), - MonomialTermExpression((1, self.m.b[2])), - MonomialTermExpression((1, self.m.b[3])), - MonomialTermExpression((1, self.m.b[4])), - MonomialTermExpression((1, self.m.b[5])), + self.m.a[1], + self.m.a[2], + self.m.a[3], + self.m.a[4], + self.m.a[5], + self.m.b[1], + self.m.b[2], + self.m.b[3], + self.m.b[4], + self.m.b[5], ] ), ) @@ -3912,13 +3730,7 @@ def test_deprecation(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3928,13 +3740,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -4156,15 +3962,15 @@ def test_SumExpression(self): self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) expr1 += self.m.b self.assertEqual(expr1(), 25) self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) # total = counter.count - start self.assertEqual(total, 1) @@ -4341,9 +4147,9 @@ def test_productOfExpressions(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) - self.assertIs(expr1.arg(1).arg(0).arg(1), expr2.arg(1).arg(0).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) + self.assertIs(expr1.arg(1).arg(0), expr2.arg(1).arg(0)) expr1 *= self.m.b self.assertEqual(expr1(), 1500) @@ -4382,8 +4188,8 @@ def test_productOfExpressions_div(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) expr1 /= self.m.b self.assertAlmostEqual(expr1(), 0.15) @@ -5214,18 +5020,7 @@ def test_pow_other(self): e += m.v[0] + m.v[1] e = m.v[0] ** e self.assertExpressionsEqual( - e, - PowExpression( - ( - m.v[0], - LinearExpression( - [ - MonomialTermExpression((1, m.v[0])), - MonomialTermExpression((1, m.v[1])), - ] - ), - ) - ), + e, PowExpression((m.v[0], LinearExpression([m.v[0], m.v[1]]))) ) diff --git a/pyomo/core/tests/unit/test_numeric_expr_api.py b/pyomo/core/tests/unit/test_numeric_expr_api.py index 4e0af126315..923f78af1be 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_api.py +++ b/pyomo/core/tests/unit/test_numeric_expr_api.py @@ -223,7 +223,7 @@ def test_negation(self): self.assertEqual(is_fixed(e), False) self.assertEqual(value(e), -15) self.assertEqual(str(e), "- (x + 2*x)") - self.assertEqual(e.to_string(verbose=True), "neg(sum(mon(1, x), mon(2, x)))") + self.assertEqual(e.to_string(verbose=True), "neg(sum(x, mon(2, x)))") # This can't occur through operator overloading, but could # through expression substitution @@ -634,8 +634,7 @@ def test_linear(self): self.assertEqual(value(e), 1 + 4 + 5 + 2) self.assertEqual(str(e), "0*x[0] + x[1] + 2*x[2] + 5 + y - 3") self.assertEqual( - e.to_string(verbose=True), - "sum(mon(0, x[0]), mon(1, x[1]), mon(2, x[2]), 5, mon(1, y), -3)", + e.to_string(verbose=True), "sum(mon(0, x[0]), x[1], mon(2, x[2]), 5, y, -3)" ) self.assertIs(type(e), LinearExpression) @@ -701,7 +700,7 @@ def test_expr_if(self): ) self.assertEqual( e.to_string(verbose=True), - "Expr_if( ( 5 <= y ), then=( sum(mon(1, x[0]), 5) ), else=( pow(x[1], 2) ) )", + "Expr_if( ( 5 <= y ), then=( sum(x[0], 5) ), else=( pow(x[1], 2) ) )", ) m.y.fix() @@ -972,9 +971,7 @@ def test_sum(self): f = e.create_node_with_local_data((m.p, m.x)) self.assertIsNot(f, e) self.assertIs(type(f), LinearExpression) - assertExpressionsStructurallyEqual( - self, f.args, [m.p, MonomialTermExpression((1, m.x))] - ) + assertExpressionsStructurallyEqual(self, f.args, [m.p, m.x]) f = e.create_node_with_local_data((m.p, m.x**2)) self.assertIsNot(f, e) diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 7c6e2af9974..bb7a291e67d 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -123,8 +123,6 @@ def setUp(self): self.mutable_l3 = _MutableNPVSumExpression([self.npv]) # often repeated reference expressions - self.mon_bin = MonomialTermExpression((1, self.bin)) - self.mon_var = MonomialTermExpression((1, self.var)) self.minus_bin = MonomialTermExpression((-1, self.bin)) self.minus_npv = NPV_NegationExpression((self.npv,)) self.minus_param_mut = NPV_NegationExpression((self.param_mut,)) @@ -368,38 +366,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -408,7 +402,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -416,13 +410,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -462,7 +452,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -471,7 +461,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -494,7 +484,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -503,7 +493,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -530,7 +520,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -539,7 +529,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -570,7 +560,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -579,7 +569,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -605,7 +595,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -619,11 +609,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -674,37 +660,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -712,7 +682,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -720,13 +690,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -737,7 +703,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -751,11 +717,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -813,7 +775,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -827,11 +789,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -882,11 +840,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -899,7 +853,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -949,7 +903,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -963,11 +917,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -1134,7 +1084,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -1159,7 +1109,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1341,7 +1291,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1350,7 +1300,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1380,7 +1330,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1409,7 +1359,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1515,32 +1465,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1551,7 +1501,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1559,12 +1509,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1837,35 +1787,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1879,7 +1825,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1887,13 +1833,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6511,7 +6453,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6520,7 +6462,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6546,7 +6488,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: @@ -6559,7 +6501,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6602,7 +6544,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6611,7 +6553,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6634,81 +6576,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) @@ -6854,7 +6784,7 @@ def as_numeric(self): assertExpressionsEqual(self, PowExpression((self.var, 2)), e) e = obj + obj - assertExpressionsEqual(self, LinearExpression((self.mon_var, self.mon_var)), e) + assertExpressionsEqual(self, LinearExpression((self.var, self.var)), e) def test_categorize_arg_type(self): class CustomAsNumeric(NumericValue): diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 34d2e1cc2c2..19968640a21 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -102,38 +102,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -142,7 +138,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -150,13 +146,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -196,7 +188,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -205,7 +197,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -228,7 +220,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -237,7 +229,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -264,7 +256,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -273,7 +265,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -304,7 +296,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -313,7 +305,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -339,7 +331,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -353,11 +345,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -408,37 +396,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -446,7 +418,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -454,13 +426,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -471,7 +439,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -485,11 +453,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -547,7 +511,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -561,11 +525,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -616,11 +576,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -633,7 +589,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -683,7 +639,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -697,11 +653,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -868,7 +820,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -893,7 +845,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1075,7 +1027,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1084,7 +1036,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1114,7 +1066,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1143,7 +1095,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1249,32 +1201,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1285,7 +1237,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1293,12 +1245,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1571,35 +1523,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1613,7 +1561,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1621,13 +1569,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6039,7 +5983,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6048,7 +5992,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6074,7 +6018,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: @@ -6087,7 +6031,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6130,7 +6074,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6139,7 +6083,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6162,81 +6106,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index fada7d6f6b2..12fb98d1d19 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -437,9 +437,7 @@ def test_replacement_linear_expression_with_constant(self): sub_map = dict() sub_map[id(m.x)] = 5 e2 = replace_expressions(e, sub_map) - assertExpressionsEqual( - self, e2, LinearExpression([10, MonomialTermExpression((1, m.y))]) - ) + assertExpressionsEqual(self, e2, LinearExpression([10, m.y])) e = LinearExpression(linear_coefs=[2, 3], linear_vars=[m.x, m.y]) sub_map = dict() @@ -886,20 +884,7 @@ def test_replace(self): assertExpressionsEqual( self, SumExpression( - [ - LinearExpression( - [ - MonomialTermExpression((1, m.y[1])), - MonomialTermExpression((1, m.y[2])), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.y[2])), - MonomialTermExpression((1, m.y[3])), - ] - ), - ] + [LinearExpression([m.y[1], m.y[2]]), LinearExpression([m.y[2], m.y[3]])] ) == 0, f, @@ -930,9 +915,7 @@ def test_npv_sum(self): e3 = replace_expressions(e1, {id(m.p1): m.x}) assertExpressionsEqual(self, e2, m.p2 + 2) - assertExpressionsEqual( - self, e3, LinearExpression([MonomialTermExpression((1, m.x)), 2]) - ) + assertExpressionsEqual(self, e3, LinearExpression([m.x, 2])) def test_npv_negation(self): m = ConcreteModel() diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 28025816262..5d0d6f6c21b 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -425,12 +425,7 @@ def check_two_term_disjunction_xor(self, xor, disj1, disj2): assertExpressionsEqual( self, xor.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, disj1.binary_indicator_var)), - EXPR.MonomialTermExpression((1, disj2.binary_indicator_var)), - ] - ), + EXPR.LinearExpression([disj1.binary_indicator_var, disj2.binary_indicator_var]), ) self.assertEqual(xor.lower, 1) self.assertEqual(xor.upper, 1) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 2383d4587f5..c6ac49f6d36 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -155,10 +155,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index aa846c4710a..ae2c44b899e 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -146,10 +146,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_disjunct.py b/pyomo/gdp/tests/test_disjunct.py index d969b245ee7..f93ac31fb0f 100644 --- a/pyomo/gdp/tests/test_disjunct.py +++ b/pyomo/gdp/tests/test_disjunct.py @@ -632,19 +632,13 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = m.iv + 1 - assertExpressionsEqual( - self, e, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): e = m.iv - 1 - assertExpressionsEqual( - self, - e, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -665,9 +659,7 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = 1 + m.iv - assertExpressionsEqual( - self, e, EXPR.LinearExpression([1, EXPR.MonomialTermExpression((1, m.biv))]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([1, m.biv])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -699,20 +691,14 @@ def test_cast_to_binary(self): with LoggingIntercept(out): a = m.iv a += 1 - assertExpressionsEqual( - self, a, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): a = m.iv a -= 1 - assertExpressionsEqual( - self, - a, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() From 7299a79db5481a5ac4faf683d1d71bba074c01b7 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 13:05:52 -0700 Subject: [PATCH 0641/1178] remove redundant implementation of acceptChildResult --- pyomo/util/subsystems.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 9a2a8b6635d..35deaab7605 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -59,9 +59,6 @@ def finalizeResult(self, result): def enterNode(self, node): pass - def acceptChildResult(self, node, data, child_result, child_idx): - pass - def acceptChildResult(self, node, data, child_result, child_idx): if child_result.__class__ in native_types: return False, None From 8b66e10c08ac149272ebd94b7f847cfed8dba7b6 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 13:06:47 -0700 Subject: [PATCH 0642/1178] comment out unnecessary walker methods --- pyomo/util/subsystems.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 35deaab7605..58921b37cca 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -56,13 +56,13 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - def enterNode(self, node): - pass + #def enterNode(self, node): + # pass - def acceptChildResult(self, node, data, child_result, child_idx): - if child_result.__class__ in native_types: - return False, None - return child_result.is_expression_type(), None + #def acceptChildResult(self, node, data, child_result, child_idx): + # if child_result.__class__ in native_types: + # return False, None + # return child_result.is_expression_type(), None def identify_external_functions(expr): From e7a4c948e7e05455f44745c472b9af4f5265edd2 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 16:09:42 -0700 Subject: [PATCH 0643/1178] add failing test --- pyomo/contrib/appsi/tests/test_fbbt.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyomo/contrib/appsi/tests/test_fbbt.py b/pyomo/contrib/appsi/tests/test_fbbt.py index a3f520e7bd6..97af611c572 100644 --- a/pyomo/contrib/appsi/tests/test_fbbt.py +++ b/pyomo/contrib/appsi/tests/test_fbbt.py @@ -151,3 +151,16 @@ def test_named_exprs(self): for x in m.x.values(): self.assertAlmostEqual(x.lb, 0) self.assertAlmostEqual(x.ub, 0) + + def test_named_exprs_nest(self): + # test for issue #3184 + m = pe.ConcreteModel() + m.x = pe.Var() + m.e = pe.Expression(expr=m.x + 1) + m.f = pe.Expression(expr=m.e) + m.c = pe.Constraint(expr=(0, m.f, 0)) + it = appsi.fbbt.IntervalTightener() + it.perform_fbbt(m) + for x in m.x.values(): + self.assertAlmostEqual(x.lb, -1) + self.assertAlmostEqual(x.ub, -1) From cd8c6ae6a9e30c4ce8f998252a8b82e4b80bfc93 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 16:12:08 -0700 Subject: [PATCH 0644/1178] apply patch --- pyomo/contrib/appsi/cmodel/src/expression.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index 234ef47e86f..f1446c6a21b 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1789,7 +1789,8 @@ int build_expression_tree(py::handle pyomo_expr, if (expr_types.expr_type_map[py::type::of(pyomo_expr)].cast() == named_expr) - pyomo_expr = pyomo_expr.attr("expr"); + return build_expression_tree(pyomo_expr.attr("expr"), appsi_expr, var_map, + param_map, expr_types); if (appsi_expr->is_leaf()) { ; From 46b89e657ac60867fa29f4c763bf8521097d3426 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 16:37:47 -0700 Subject: [PATCH 0645/1178] Change around the logic to use nobjectives --- pyomo/contrib/solver/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 54871e90c2f..55b013facb1 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -41,7 +41,8 @@ class SolverBase(abc.ABC): """ This base class defines the methods required for all solvers: - - available: Determines whether the solver is able to be run, combining both whether it can be found on the system and if the license is valid. + - available: Determines whether the solver is able to be run, + combining both whether it can be found on the system and if the license is valid. - solve: The main method of every solver - version: The version of the solver - is_persistent: Set to false for all non-persistent solvers. @@ -420,12 +421,11 @@ def _map_results(self, model, results): legacy_results.solver.termination_message = str(results.termination_condition) legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() - if model.nobjectives() == 0: - legacy_results.problem.number_of_objectives = 0 - else: + number_of_objectives = model.nobjectives() + legacy_results.problem.number_of_objectives = number_of_objectives + if number_of_objectives > 0: obj = get_objective(model) legacy_results.problem.sense = obj.sense - legacy_results.problem.number_of_objectives = len(obj) if obj.sense == minimize: legacy_results.problem.lower_bound = results.objective_bound From 70f225e6c0f854252fb94ecfb1eef0d58a6b818d Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 16:46:08 -0700 Subject: [PATCH 0646/1178] apply black --- pyomo/util/subsystems.py | 7 ++++--- pyomo/util/tests/test_subsystems.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 58921b37cca..ff5f6dedd58 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -25,7 +25,6 @@ class _ExternalFunctionVisitor(StreamBasedExpressionVisitor): - def __init__(self, descend_into_named_expressions=True): super().__init__() self._descend_into_named_expressions = descend_into_named_expressions @@ -56,10 +55,10 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - #def enterNode(self, node): + # def enterNode(self, node): # pass - #def acceptChildResult(self, node, data, child_result, child_idx): + # def acceptChildResult(self, node, data, child_result, child_idx): # if child_result.__class__ in native_types: # return False, None # return child_result.is_expression_type(), None @@ -114,6 +113,8 @@ def add_local_external_functions(block): from pyomo.common.timing import HierarchicalTimer + + def create_subsystem_block( constraints, variables=None, diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index f102670ba62..a9b8a215fcc 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -304,11 +304,11 @@ def _make_model_with_external_functions(self, named_expressions=False): m.subexpr = pyo.Expression(pyo.PositiveIntegers) subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3 ** 2 + subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3**2 else: subexpr1 = 2 * m.fermi(m.v1) subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[2] + m.v3 ** 2 + subexpr3 = m.subexpr[2] + m.v3**2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) From 1c6a6c79ec2297c142367da385145c6ce7ba4884 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 7 Mar 2024 17:02:03 -0700 Subject: [PATCH 0647/1178] type hints --- pyomo/core/base/block.py | 5 ++++- pyomo/core/base/constraint.py | 5 ++++- pyomo/core/base/set.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 908e0ef1abd..f3d9c7458e1 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2265,8 +2265,11 @@ class IndexedBlock(Block): def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) + @overload def __getitem__(self, index) -> _BlockData: - return super().__getitem__(index) + ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore # diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index a36bc679e49..899bc8c9499 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1033,8 +1033,11 @@ def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) + @overload def __getitem__(self, index) -> _GeneralConstraintData: - return super().__getitem__(index) + ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore @ModelComponentFactory.register("A list of constraint expressions.") diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b8ddae14e9f..9217c09866e 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2382,8 +2382,11 @@ def data(self): "Return a dict containing the data() of each Set in this IndexedSet" return {k: v.data() for k, v in self.items()} + @overload def __getitem__(self, index) -> _SetData: - return super().__getitem__(index) + ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore class FiniteScalarSet(_FiniteSetData, Set): From 00d2a977471d04a032079c374eb6f4e9e7f2d6cc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 7 Mar 2024 17:06:30 -0700 Subject: [PATCH 0648/1178] run black --- pyomo/core/base/block.py | 3 +-- pyomo/core/base/constraint.py | 3 +-- pyomo/core/base/set.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index f3d9c7458e1..2918ef78b00 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2266,8 +2266,7 @@ def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) @overload - def __getitem__(self, index) -> _BlockData: - ... + def __getitem__(self, index) -> _BlockData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 899bc8c9499..8916777e9c8 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1034,8 +1034,7 @@ def add(self, index, expr): return self.__setitem__(index, expr) @overload - def __getitem__(self, index) -> _GeneralConstraintData: - ... + def __getitem__(self, index) -> _GeneralConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 9217c09866e..b3277ab3260 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2383,8 +2383,7 @@ def data(self): return {k: v.data() for k, v in self.items()} @overload - def __getitem__(self, index) -> _SetData: - ... + def __getitem__(self, index) -> _SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore From b96d12eea18f9c0bf570ef0b420e4fcbd0f26370 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 17:17:27 -0700 Subject: [PATCH 0649/1178] arguments on one line to keep black happy --- pyomo/contrib/incidence_analysis/scc_solver.py | 6 +----- pyomo/util/subsystems.py | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 6c556646a8c..86b02c94194 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -26,11 +26,7 @@ def generate_strongly_connected_components( - constraints, - variables=None, - include_fixed=False, - igraph=None, - timer=None, + constraints, variables=None, include_fixed=False, igraph=None, timer=None ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index ff5f6dedd58..79fbdd2d281 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -75,8 +75,7 @@ def add_local_external_functions(block): named_expressions = [] visitor = _ExternalFunctionVisitor(descend_into_named_expressions=False) for comp in block.component_data_objects( - (Constraint, Expression, Objective), - active=True, + (Constraint, Expression, Objective), active=True ): ef_exprs.extend(visitor.walk_expression(comp.expr)) named_expr_set = ComponentSet(visitor.named_expressions) @@ -116,10 +115,7 @@ def add_local_external_functions(block): def create_subsystem_block( - constraints, - variables=None, - include_fixed=False, - timer=None, + constraints, variables=None, include_fixed=False, timer=None ): """This function creates a block to serve as a subsystem with the specified variables and constraints. To satisfy certain writers, other From 67b9b2f958772e3e0fe699c0ca687912cf5585a4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 17:37:13 -0700 Subject: [PATCH 0650/1178] Update PyROS to admit VarData in LinearExpressions --- .../contrib/pyros/pyros_algorithm_methods.py | 20 ++++++---- pyomo/contrib/pyros/tests/test_grcs.py | 40 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 45b652447ff..f847a3a73dc 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -26,6 +26,7 @@ ) from pyomo.contrib.pyros.util import get_main_elapsed_time, coefficient_matching from pyomo.core.base import value +from pyomo.core.expr import MonomialTermExpression from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.base.var import _VarData as VarData from itertools import chain @@ -69,14 +70,17 @@ def get_dr_var_to_scaled_expr_map( ssv_dr_eq_zip = zip(second_stage_vars, decision_rule_eqns) for ssv_idx, (ssv, dr_eq) in enumerate(ssv_dr_eq_zip): for term in dr_eq.body.args: - is_ssv_term = ( - isinstance(term.args[0], int) - and term.args[0] == -1 - and isinstance(term.args[1], VarData) - ) - if not is_ssv_term: - dr_var = term.args[1] - var_to_scaled_expr_map[dr_var] = term + if isinstance(term, MonomialTermExpression): + is_ssv_term = ( + isinstance(term.args[0], int) + and term.args[0] == -1 + and isinstance(term.args[1], VarData) + ) + if not is_ssv_term: + dr_var = term.args[1] + var_to_scaled_expr_map[dr_var] = term + elif isinstance(term, VarData): + var_to_scaled_expr_map[term] = MonomialTermExpression((1, term)) return var_to_scaled_expr_map diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index df3568e42a4..c308f0d6990 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -19,6 +19,7 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers +from pyomo.core.base.var import _VarData from pyomo.core.expr import ( identify_variables, identify_mutable_parameters, @@ -571,22 +572,30 @@ def test_dr_eqns_form_correct(self): dr_polynomial_terms, indexed_dr_var.values(), dr_monomial_param_combos ) for idx, (term, dr_var, param_combo) in enumerate(dr_polynomial_zip): - # term should be a monomial expression of form - # (uncertain parameter product) * (decision rule variable) - # so length of expression object should be 2 - self.assertEqual( - len(term.args), - 2, - msg=( - f"Length of `args` attribute of term {str(term)} " - f"of DR equation {dr_eq.name!r} is not as expected. " - f"Args: {term.args}" - ), - ) + # term should be either a monomial expression or scalar variable + if isinstance(term, MonomialTermExpression): + # should be of form (uncertain parameter product) * + # (decision rule variable) so length of expression + # object should be 2 + self.assertEqual( + len(term.args), + 2, + msg=( + f"Length of `args` attribute of term {str(term)} " + f"of DR equation {dr_eq.name!r} is not as expected. " + f"Args: {term.args}" + ), + ) + + # check that uncertain parameters participating in + # the monomial are as expected + param_product_multiplicand = term.args[0] + dr_var_multiplicand = term.args[1] + else: + self.assertIsInstance(term, _VarData) + param_product_multiplicand = 1 + dr_var_multiplicand = term - # check that uncertain parameters participating in - # the monomial are as expected - param_product_multiplicand = term.args[0] if idx == 0: # static DR term param_combo_found_in_term = (param_product_multiplicand,) @@ -612,7 +621,6 @@ def test_dr_eqns_form_correct(self): # check that DR variable participating in the monomial # is as expected - dr_var_multiplicand = term.args[1] self.assertIs( dr_var_multiplicand, dr_var, From ec4f733a1b6c7de8ef9f624ece8befd78f081128 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 18:02:27 -0700 Subject: [PATCH 0651/1178] use_calc_var default should be True --- pyomo/contrib/incidence_analysis/scc_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 86b02c94194..76eb7f91cb0 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -100,7 +100,7 @@ def solve_strongly_connected_components( *, solver=None, solve_kwds=None, - use_calc_var=False, + use_calc_var=True, calc_var_kwds=None, timer=None, ): From cd54e6f72563f46adbfd8b61ac76e6d7f0d53824 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 18:05:22 -0700 Subject: [PATCH 0652/1178] re-add exception I accidentally deleted --- pyomo/contrib/incidence_analysis/scc_solver.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 76eb7f91cb0..eff4f5ae5fa 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -175,7 +175,15 @@ def solve_strongly_connected_components( ) timer.stop("calc-var-from-con") else: - inputs = list(scc.input_vars.values()) + if solver is None: + var_names = [var.name for var in scc.vars.values()][:10] + con_names = [con.name for con in scc.cons.values()][:10] + raise RuntimeError( + "An external solver is required if block has strongly\n" + "connected components of size greater than one (is not" + " a DAG).\nGot an SCC of size %sx%s including" + " components:\n%s\n%s" % (N, N, var_names, con_names) + ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") timer.start("scc-subsolver") From c7b34d043876653b8a53d6d0aeca3937d45e5319 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 18:08:39 -0700 Subject: [PATCH 0653/1178] Resolve incompatibility with Python<=3.10 --- pyomo/core/expr/template_expr.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index 6ac4c8c041f..a7f301e32f1 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -117,18 +117,10 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0][*args[1:]] + return args[0].__getitem__(args[1:]) def _apply_operation(self, result): - args = tuple( - ( - arg - if arg.__class__ in native_types or not arg.is_numeric_type() - else value(arg) - ) - for arg in result[1:] - ) - return result[0][*result[1:]] + return result[0].__getitem__(result[1:]) class Numeric_GetItemExpression(GetItemExpression, NumericExpression): From fadca016f1efcf6f59bdfce2c6f7a5e926836a0e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 18:09:06 -0700 Subject: [PATCH 0654/1178] Minor code readibility improvement --- pyomo/core/expr/template_expr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index a7f301e32f1..d30046e9d82 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -251,8 +251,8 @@ def nargs(self): return 2 def _apply_operation(self, result): - assert len(result) == 2 - return getattr(result[0], result[1]) + obj, attr = result + return getattr(obj, attr) def _to_string(self, values, verbose, smap): assert len(values) == 2 From f7b70038e58fe5c0eb3a98a8eaf005cb6113ae0a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 21:09:54 -0700 Subject: [PATCH 0655/1178] Update doc tests to track change in LinearExpression arg types --- doc/OnlineDocs/src/expr/managing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py index 00d521d16ab..ff149e4fd5c 100644 --- a/doc/OnlineDocs/src/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -181,7 +181,7 @@ def clone_expression(expr): # x[0] + 5*x[1] print(str(ce)) # x[0] + 5*x[1] -print(e.arg(0) is not ce.arg(0)) +print(e.arg(0) is ce.arg(0)) # True print(e.arg(1) is not ce.arg(1)) # True From 7e66907075ccc0dc304270ebd059afda95012c5a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 8 Mar 2024 09:23:26 -0700 Subject: [PATCH 0656/1178] NFC: update docs --- pyomo/core/expr/numeric_expr.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 2cf4073b49f..9b624d2b8bd 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1234,9 +1234,11 @@ class LinearExpression(SumExpression): """An expression object for linear polynomials. This is a derived :py:class`SumExpression` that guarantees all - arguments are either not potentially variable (e.g., native types, - Params, or NPV expressions) OR :py:class:`MonomialTermExpression` - objects. + arguments are one of the following types: + + - not potentially variable (e.g., native types, Params, or NPV expressions) + - :py:class:`MonomialTermExpression` + - :py:class:`_VarData` Args: args (tuple): Children nodes @@ -1253,7 +1255,7 @@ def __init__(self, args=None, constant=None, linear_coefs=None, linear_vars=None You can specify `args` OR (`constant`, `linear_coefs`, and `linear_vars`). If `args` is provided, it should be a list that - contains only constants, NPV objects/expressions, or + contains only constants, NPV objects/expressions, variables, or :py:class:`MonomialTermExpression` objects. Alternatively, you can specify the constant, the list of linear_coefs and the list of linear_vars separately. Note that these lists are NOT From e5c8027420cb29333ba4e9fe30c5617387568c6f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 8 Mar 2024 14:13:07 -0700 Subject: [PATCH 0657/1178] Add missing import --- pyomo/core/base/constraint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8916777e9c8..fde1160e563 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -44,6 +44,7 @@ ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper, + IndexedComponent, ) from pyomo.core.base.set import Set from pyomo.core.base.disable_methods import disable_methods From 476fa8d7bdc80c37ba0d3aeca5a2a9c5d586c849 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:11:35 -0700 Subject: [PATCH 0658/1178] Allow bare variables in LinearExpression nodes --- pyomo/core/expr/numeric_expr.py | 39 ++++++++++++++++-------------- pyomo/repn/linear.py | 38 ++++++++++++++++------------- pyomo/repn/plugins/baron_writer.py | 19 ++++++++++++--- pyomo/repn/plugins/gams_writer.py | 10 +++++++- pyomo/repn/plugins/nl_writer.py | 14 +++++++++++ pyomo/repn/quadratic.py | 25 +++++++------------ pyomo/repn/standard_repn.py | 22 +++++++++++++++++ pyomo/repn/tests/test_linear.py | 2 +- 8 files changed, 112 insertions(+), 57 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index c1199ffdcad..8ce7ee81c9a 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1298,8 +1298,14 @@ def _build_cache(self): if arg.__class__ is MonomialTermExpression: coef.append(arg._args_[0]) var.append(arg._args_[1]) - else: + elif arg.__class__ in native_numeric_types: const += arg + elif not arg.is_potentially_variable(): + const += arg + else: + assert arg.is_potentially_variable() + coef.append(1) + var.append(arg) LinearExpression._cache = (self, const, coef, var) @property @@ -1325,7 +1331,7 @@ def create_node_with_local_data(self, args, classtype=None): classtype = self.__class__ if type(args) is not list: args = list(args) - for i, arg in enumerate(args): + for arg in args: if arg.__class__ in self._allowable_linear_expr_arg_types: # 99% of the time, the arg type hasn't changed continue @@ -1336,8 +1342,7 @@ def create_node_with_local_data(self, args, classtype=None): # NPV expressions are OK pass elif arg.is_variable_type(): - # vars are OK, but need to be mapped to monomial terms - args[i] = MonomialTermExpression((1, arg)) + # vars are OK continue else: # For anything else, convert this to a general sum @@ -1820,7 +1825,7 @@ def _add_native_param(a, b): def _add_native_var(a, b): if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_native_monomial(a, b): @@ -1871,7 +1876,7 @@ def _add_npv_param(a, b): def _add_npv_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_npv_monomial(a, b): @@ -1929,7 +1934,7 @@ def _add_param_var(a, b): a = a.value if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_param_monomial(a, b): @@ -1972,11 +1977,11 @@ def _add_param_other(a, b): def _add_var_native(a, b): if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_npv(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_param(a, b): @@ -1984,21 +1989,19 @@ def _add_var_param(a, b): b = b.value if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_var(a, b): - return LinearExpression( - [MonomialTermExpression((1, a)), MonomialTermExpression((1, b))] - ) + return LinearExpression([a, b]) def _add_var_monomial(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_linear(a, b): - return b._trunc_append(MonomialTermExpression((1, a))) + return b._trunc_append(a) def _add_var_sum(a, b): @@ -2033,7 +2036,7 @@ def _add_monomial_param(a, b): def _add_monomial_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_monomial_monomial(a, b): @@ -2076,7 +2079,7 @@ def _add_linear_param(a, b): def _add_linear_var(a, b): - return a._trunc_append(MonomialTermExpression((1, b))) + return a._trunc_append(b) def _add_linear_monomial(a, b): @@ -2401,7 +2404,7 @@ def _iadd_mutablelinear_param(a, b): def _iadd_mutablelinear_var(a, b): - a._args_.append(MonomialTermExpression((1, b))) + a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 6ab4abfdaf5..d601ccbcd7c 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -31,8 +31,8 @@ MonomialTermExpression, LinearExpression, SumExpression, - NPV_SumExpression, ExternalFunctionExpression, + mutable_expression, ) from pyomo.core.expr.relational_expr import ( EqualityExpression, @@ -120,22 +120,14 @@ def to_expression(self, visitor): ans = 0 if self.linear: var_map = visitor.var_map - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: @@ -704,6 +696,18 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_numeric_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + const += visitor.check_constant(arg.value, arg) + continue + LinearBeforeChildDispatcher._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index de19b5aad73..ab673b0c1c3 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -174,15 +174,26 @@ def _monomial_to_string(self, node): return self.smap.getSymbol(var) return ftoa(const, True) + '*' + self.smap.getSymbol(var) + def _var_to_string(self, node): + if node.is_fixed(): + return ftoa(node.value, True) + self.variables.add(id(node)) + return self.smap.getSymbol(node) + def _linear_to_string(self, node): values = [ ( self._monomial_to_string(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + if arg.__class__ is EXPR.MonomialTermExpression + else ( + ftoa(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_string(arg) + if arg.is_variable_type() + else ftoa(value(arg), True) + ) ) - else ftoa(value(arg)) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 5f94f176762..0756cb64920 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -183,7 +183,15 @@ def _linear_to_string(self, node): ( self._monomial_to_string(arg) if arg.__class__ is EXPR.MonomialTermExpression - else ftoa(arg, True) + else ( + ftoa(arg, True) + if arg.__class__ in native_numeric_types + else ( + self.smap.getSymbol(arg) + if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + else ftoa(value(arg), True) + ) + ) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a256cd1b900..b82d4df77e2 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2780,6 +2780,20 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg) + const += visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index c538d1efc7f..0ddfda829ed 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -98,22 +98,15 @@ def to_expression(self, visitor): e += coef * (var_map[x1] * var_map[x2]) ans += e if self.linear: - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + var_map = visitor.var_map + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 8700872f04f..8600a8a50f6 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -321,6 +321,16 @@ def generate_standard_repn( linear_vars[id_] = v elif arg.__class__ in native_numeric_types: C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg.value + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += EXPR.evaluate_expression(arg) else: # compute_values == False @@ -336,6 +346,18 @@ def generate_standard_repn( else: linear_coefs[id_] = c linear_vars[id_] = v + elif arg.__class__ in native_numeric_types: + C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += arg diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 6843650d0c2..0fd428fd8ee 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1589,7 +1589,7 @@ def test_to_expression(self): expr.constant = 0 expr.linear[id(m.x)] = 0 expr.linear[id(m.y)] = 0 - assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression()) + assertExpressionsEqual(self, expr.to_expression(visitor), 0) @unittest.skipUnless(numpy_available, "Test requires numpy") def test_nonnumeric(self): From 0785422bb3f97359a48128434ddbeb4e99f6c5b1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:34:52 -0700 Subject: [PATCH 0659/1178] NFC: apply black --- pyomo/repn/plugins/gams_writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 0756cb64920..a0f407d7952 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -188,7 +188,8 @@ def _linear_to_string(self, node): if arg.__class__ in native_numeric_types else ( self.smap.getSymbol(arg) - if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + if arg.is_variable_type() + and (not arg.fixed or self.output_fixed_variables) else ftoa(value(arg), True) ) ) From 1507ffd2a6c32ca4d5352488997dddab251d282d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 12:58:56 -0700 Subject: [PATCH 0660/1178] Update tests to track change in LinearExpression arg types --- pyomo/core/tests/transform/test_add_slacks.py | 56 +-- pyomo/core/tests/unit/test_compare.py | 6 - pyomo/core/tests/unit/test_expression.py | 11 +- pyomo/core/tests/unit/test_numeric_expr.py | 329 ++++-------------- .../core/tests/unit/test_numeric_expr_api.py | 11 +- .../unit/test_numeric_expr_dispatcher.py | 278 ++++++--------- .../unit/test_numeric_expr_zerofilter.py | 274 ++++++--------- pyomo/core/tests/unit/test_visitor.py | 23 +- pyomo/gdp/tests/common_tests.py | 7 +- pyomo/gdp/tests/test_bigm.py | 5 +- pyomo/gdp/tests/test_binary_multiplication.py | 5 +- pyomo/gdp/tests/test_disjunct.py | 24 +- 12 files changed, 302 insertions(+), 727 deletions(-) diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index 7896cab7e88..a74a9b75c4f 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -102,10 +102,7 @@ def checkRule1(self, m): self, cons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1)), - ] + [m.x, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1))] ), ) @@ -118,14 +115,7 @@ def checkRule3(self, m): self.assertEqual(cons.lower, 0.1) assertExpressionsEqual( - self, - cons.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] - ), + self, cons.body, EXPR.LinearExpression([m.x, transBlock._slack_plus_rule3]) ) def test_ub_constraint_modified(self): @@ -154,8 +144,8 @@ def test_both_bounds_constraint_modified(self): cons.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), + m.y, + transBlock._slack_plus_rule2, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule2)), ] ), @@ -184,10 +174,10 @@ def test_new_obj_created(self): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), + transBlock._slack_minus_rule1, + transBlock._slack_plus_rule2, + transBlock._slack_minus_rule2, + transBlock._slack_plus_rule3, ] ), ) @@ -302,10 +292,7 @@ def checkTargetsObj(self, m): self, obj.expr, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] + [transBlock._slack_minus_rule1, transBlock._slack_plus_rule3] ), ) @@ -423,9 +410,9 @@ def test_transformed_constraints_sumexpression_body(self): c.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.x)), + m.x, EXPR.MonomialTermExpression((-2, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule4)), + transBlock._slack_plus_rule4, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule4)), ] ), @@ -518,15 +505,9 @@ def checkTargetObj(self, m): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[1]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[2]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[3]")) - ), + transBlock.component("_slack_plus_rule1[1]"), + transBlock.component("_slack_plus_rule1[2]"), + transBlock.component("_slack_plus_rule1[3]"), ] ), ) @@ -558,14 +539,7 @@ def checkTransformedRule1(self, m, i): EXPR.LinearExpression( [ EXPR.MonomialTermExpression((2, m.x[i])), - EXPR.MonomialTermExpression( - ( - 1, - m._core_add_slack_variables.component( - "_slack_plus_rule1[%s]" % i - ), - ) - ), + m._core_add_slack_variables.component("_slack_plus_rule1[%s]" % i), ] ), ) diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py index f80753bdb61..7c3536bc084 100644 --- a/pyomo/core/tests/unit/test_compare.py +++ b/pyomo/core/tests/unit/test_compare.py @@ -165,17 +165,11 @@ def test_expr_if(self): 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, - (MonomialTermExpression, 2), - 1, m.x, 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, (MonomialTermExpression, 2), -1, diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index c9afc6a1f76..678df4c01a8 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -738,10 +738,10 @@ def test_pprint_oldStyle(self): expr = model.e * model.x**2 + model.E[1] output = """\ -sum(prod(e{sum(mon(1, x), 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) +sum(prod(e{sum(x, 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) e : Size=1, Index=None Key : Expression - None : sum(mon(1, x), 2) + None : sum(x, 2) E : Size=2, Index={1, 2} Key : Expression 1 : sum(pow(x, 2), 1) @@ -951,12 +951,7 @@ def test_isub(self): assertExpressionsEqual( self, m.e.expr, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, m.y)), - ] - ), + EXPR.LinearExpression([m.x, EXPR.MonomialTermExpression((-1, m.y))]), ) self.assertTrue(compare_expressions(m.e.expr, m.x - m.y)) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index c1066c292d7..968b3acb6a4 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -638,12 +638,7 @@ def test_simpleSum(self): m.b = Var() e = m.a + m.b # - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b])) self.assertRaises(KeyError, e.arg, 3) @@ -654,14 +649,7 @@ def test_simpleSum_API(self): e = m.a + m.b e += 2 * m.a self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((2, m.a)), - ] - ), + e, LinearExpression([m.a, m.b, MonomialTermExpression((2, m.a))]) ) def test_constSum(self): @@ -669,13 +657,9 @@ def test_constSum(self): m = AbstractModel() m.a = Var() # - self.assertExpressionsEqual( - m.a + 5, LinearExpression([MonomialTermExpression((1, m.a)), 5]) - ) + self.assertExpressionsEqual(m.a + 5, LinearExpression([m.a, 5])) - self.assertExpressionsEqual( - 5 + m.a, LinearExpression([5, MonomialTermExpression((1, m.a))]) - ) + self.assertExpressionsEqual(5 + m.a, LinearExpression([5, m.a])) def test_nestedSum(self): # @@ -696,12 +680,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + 5 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -710,12 +689,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = 5 + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -724,16 +698,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + m.c - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -742,16 +707,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = m.c + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -762,17 +718,7 @@ def test_nestedSum(self): e2 = m.c + m.d e = e1 + e2 # - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c, m.d])) def test_nestedSum2(self): # @@ -798,22 +744,7 @@ def test_nestedSum2(self): self.assertExpressionsEqual( e, - SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] - ), + SumExpression([ProductExpression((2, LinearExpression([m.a, m.b]))), m.c]), ) # * @@ -834,20 +765,7 @@ def test_nestedSum2(self): ( 3, SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] + [ProductExpression((2, LinearExpression([m.a, m.b]))), m.c] ), ) ), @@ -891,10 +809,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + m.b # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((5, m.a)), MonomialTermExpression((1, m.b))] - ), + e, LinearExpression([MonomialTermExpression((5, m.a)), m.b]) ) # + @@ -905,10 +820,7 @@ def test_sumOf_nestedTrivialProduct(self): e = m.b + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.b)), MonomialTermExpression((5, m.a))] - ), + e, LinearExpression([m.b, MonomialTermExpression((5, m.a))]) ) # + @@ -920,14 +832,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + e2 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) # + @@ -939,14 +844,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e2 + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) def test_simpleDiff(self): @@ -962,10 +860,7 @@ def test_simpleDiff(self): # a b e = m.a - m.b self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((-1, m.b))] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b))]) ) def test_constDiff(self): @@ -978,9 +873,7 @@ def test_constDiff(self): # - # / \ # a 5 - self.assertExpressionsEqual( - m.a - 5, LinearExpression([MonomialTermExpression((1, m.a)), -5]) - ) + self.assertExpressionsEqual(m.a - 5, LinearExpression([m.a, -5])) # - # / \ @@ -1002,10 +895,7 @@ def test_paramDiff(self): # a p e = m.a - m.p self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), NPV_NegationExpression((m.p,))] - ), + e, LinearExpression([m.a, NPV_NegationExpression((m.p,))]) ) # - @@ -1079,14 +969,7 @@ def test_nestedDiff(self): e1 = m.a - m.b e = e1 - 5 self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - -5, - ] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b)), -5]) ) # - @@ -1102,14 +985,7 @@ def test_nestedDiff(self): [ 5, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1126,7 +1002,7 @@ def test_nestedDiff(self): e, LinearExpression( [ - MonomialTermExpression((1, m.a)), + m.a, MonomialTermExpression((-1, m.b)), MonomialTermExpression((-1, m.c)), ] @@ -1146,14 +1022,7 @@ def test_nestedDiff(self): [ m.c, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1171,21 +1040,9 @@ def test_nestedDiff(self): e, SumExpression( [ - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), + LinearExpression([m.a, MonomialTermExpression((-1, m.b))]), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.c)), - MonomialTermExpression((-1, m.d)), - ] - ), - ) + (LinearExpression([m.c, MonomialTermExpression((-1, m.d))]),) ), ] ), @@ -1382,10 +1239,7 @@ def test_sumOf_nestedTrivialProduct2(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), - ] + [m.b, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a))] ), ) @@ -1403,14 +1257,7 @@ def test_sumOf_nestedTrivialProduct2(self): [ MonomialTermExpression((m.p, m.a)), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((-1, m.c)), - ] - ), - ) + (LinearExpression([m.b, MonomialTermExpression((-1, m.c))]),) ), ] ), @@ -1428,7 +1275,7 @@ def test_sumOf_nestedTrivialProduct2(self): e, LinearExpression( [ - MonomialTermExpression((1, m.b)), + m.b, MonomialTermExpression((-1, m.c)), MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), ] @@ -1598,22 +1445,7 @@ def test_nestedProduct2(self): self.assertExpressionsEqual( e, ProductExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + (LinearExpression([m.a, m.b, m.c]), LinearExpression([m.a, m.b, m.d])) ), ) # Verify shared args... @@ -1638,9 +1470,7 @@ def test_nestedProduct2(self): e3 = e1 * m.d e = e2 * e3 # - inner = LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ) + inner = LinearExpression([m.a, m.b]) self.assertExpressionsEqual( e, ProductExpression( @@ -2034,10 +1864,10 @@ def test_sum(self): model.p = Param(mutable=True) expr = 5 + model.a + model.a - self.assertEqual("sum(5, mon(1, a), mon(1, a))", str(expr)) + self.assertEqual("sum(5, a, a)", str(expr)) expr += 5 - self.assertEqual("sum(5, mon(1, a), mon(1, a), 5)", str(expr)) + self.assertEqual("sum(5, a, a, 5)", str(expr)) expr = 2 + model.p self.assertEqual("sum(2, p)", str(expr)) @@ -2053,24 +1883,18 @@ def test_linearsum(self): expr = quicksum(i * model.a[i] for i in A) self.assertEqual( - "sum(mon(0, a[0]), mon(1, a[1]), mon(2, a[2]), mon(3, a[3]), " - "mon(4, a[4]))", + "sum(mon(0, a[0]), a[1], mon(2, a[2]), mon(3, a[3]), " "mon(4, a[4]))", str(expr), ) expr = quicksum((i - 2) * model.a[i] for i in A) self.assertEqual( - "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), mon(1, a[3]), " - "mon(2, a[4]))", + "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), a[3], " "mon(2, a[4]))", str(expr), ) expr = quicksum(model.a[i] for i in A) - self.assertEqual( - "sum(mon(1, a[0]), mon(1, a[1]), mon(1, a[2]), mon(1, a[3]), " - "mon(1, a[4]))", - str(expr), - ) + self.assertEqual("sum(a[0], a[1], a[2], a[3], a[4])", str(expr)) model.p[1].value = 0 model.p[3].value = 3 @@ -2138,10 +1962,10 @@ def test_inequality(self): self.assertEqual("5 <= a < 10", str(expr)) expr = 5 <= model.a + 5 - self.assertEqual("5 <= sum(mon(1, a), 5)", str(expr)) + self.assertEqual("5 <= sum(a, 5)", str(expr)) expr = expr < 10 - self.assertEqual("5 <= sum(mon(1, a), 5) < 10", str(expr)) + self.assertEqual("5 <= sum(a, 5) < 10", str(expr)) def test_equality(self): # @@ -2166,10 +1990,10 @@ def test_equality(self): self.assertEqual("a == 10", str(expr)) expr = 5 == model.a + 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) expr = model.a + 5 == 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) def test_getitem(self): m = ConcreteModel() @@ -2206,7 +2030,7 @@ def test_small_expression(self): expr = abs(expr) self.assertEqual( "abs(neg(pow(2, div(2, prod(2, sum(1, neg(pow(div(prod(sum(" - "mon(1, a), 1, -1), a), a), b)), 1))))))", + "a, 1, -1), a), a), b)), 1))))))", str(expr), ) @@ -3754,13 +3578,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3872,16 +3690,16 @@ def test_summation_compression(self): e, LinearExpression( [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - MonomialTermExpression((1, self.m.b[1])), - MonomialTermExpression((1, self.m.b[2])), - MonomialTermExpression((1, self.m.b[3])), - MonomialTermExpression((1, self.m.b[4])), - MonomialTermExpression((1, self.m.b[5])), + self.m.a[1], + self.m.a[2], + self.m.a[3], + self.m.a[4], + self.m.a[5], + self.m.b[1], + self.m.b[2], + self.m.b[3], + self.m.b[4], + self.m.b[5], ] ), ) @@ -3912,13 +3730,7 @@ def test_deprecation(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3928,13 +3740,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -4156,15 +3962,15 @@ def test_SumExpression(self): self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) expr1 += self.m.b self.assertEqual(expr1(), 25) self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) # total = counter.count - start self.assertEqual(total, 1) @@ -4341,9 +4147,9 @@ def test_productOfExpressions(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) - self.assertIs(expr1.arg(1).arg(0).arg(1), expr2.arg(1).arg(0).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) + self.assertIs(expr1.arg(1).arg(0), expr2.arg(1).arg(0)) expr1 *= self.m.b self.assertEqual(expr1(), 1500) @@ -4382,8 +4188,8 @@ def test_productOfExpressions_div(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) expr1 /= self.m.b self.assertAlmostEqual(expr1(), 0.15) @@ -5214,18 +5020,7 @@ def test_pow_other(self): e += m.v[0] + m.v[1] e = m.v[0] ** e self.assertExpressionsEqual( - e, - PowExpression( - ( - m.v[0], - LinearExpression( - [ - MonomialTermExpression((1, m.v[0])), - MonomialTermExpression((1, m.v[1])), - ] - ), - ) - ), + e, PowExpression((m.v[0], LinearExpression([m.v[0], m.v[1]]))) ) diff --git a/pyomo/core/tests/unit/test_numeric_expr_api.py b/pyomo/core/tests/unit/test_numeric_expr_api.py index 4e0af126315..923f78af1be 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_api.py +++ b/pyomo/core/tests/unit/test_numeric_expr_api.py @@ -223,7 +223,7 @@ def test_negation(self): self.assertEqual(is_fixed(e), False) self.assertEqual(value(e), -15) self.assertEqual(str(e), "- (x + 2*x)") - self.assertEqual(e.to_string(verbose=True), "neg(sum(mon(1, x), mon(2, x)))") + self.assertEqual(e.to_string(verbose=True), "neg(sum(x, mon(2, x)))") # This can't occur through operator overloading, but could # through expression substitution @@ -634,8 +634,7 @@ def test_linear(self): self.assertEqual(value(e), 1 + 4 + 5 + 2) self.assertEqual(str(e), "0*x[0] + x[1] + 2*x[2] + 5 + y - 3") self.assertEqual( - e.to_string(verbose=True), - "sum(mon(0, x[0]), mon(1, x[1]), mon(2, x[2]), 5, mon(1, y), -3)", + e.to_string(verbose=True), "sum(mon(0, x[0]), x[1], mon(2, x[2]), 5, y, -3)" ) self.assertIs(type(e), LinearExpression) @@ -701,7 +700,7 @@ def test_expr_if(self): ) self.assertEqual( e.to_string(verbose=True), - "Expr_if( ( 5 <= y ), then=( sum(mon(1, x[0]), 5) ), else=( pow(x[1], 2) ) )", + "Expr_if( ( 5 <= y ), then=( sum(x[0], 5) ), else=( pow(x[1], 2) ) )", ) m.y.fix() @@ -972,9 +971,7 @@ def test_sum(self): f = e.create_node_with_local_data((m.p, m.x)) self.assertIsNot(f, e) self.assertIs(type(f), LinearExpression) - assertExpressionsStructurallyEqual( - self, f.args, [m.p, MonomialTermExpression((1, m.x))] - ) + assertExpressionsStructurallyEqual(self, f.args, [m.p, m.x]) f = e.create_node_with_local_data((m.p, m.x**2)) self.assertIsNot(f, e) diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3787f00de47..37833d7e8a4 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -123,8 +123,6 @@ def setUp(self): self.mutable_l3 = _MutableNPVSumExpression([self.npv]) # often repeated reference expressions - self.mon_bin = MonomialTermExpression((1, self.bin)) - self.mon_var = MonomialTermExpression((1, self.var)) self.minus_bin = MonomialTermExpression((-1, self.bin)) self.minus_npv = NPV_NegationExpression((self.npv,)) self.minus_param_mut = NPV_NegationExpression((self.param_mut,)) @@ -368,38 +366,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -408,7 +402,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -416,13 +410,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -462,7 +452,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -471,7 +461,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -494,7 +484,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -503,7 +493,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -530,7 +520,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -539,7 +529,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -570,7 +560,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -579,7 +569,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -605,7 +595,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -619,11 +609,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -674,37 +660,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -712,7 +682,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -720,13 +690,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -737,7 +703,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -751,11 +717,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -813,7 +775,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -827,11 +789,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -882,11 +840,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -899,7 +853,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -949,7 +903,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -963,11 +917,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -1134,7 +1084,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -1159,7 +1109,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1341,7 +1291,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1350,7 +1300,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1380,7 +1330,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1409,7 +1359,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1515,32 +1465,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1551,7 +1501,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1559,12 +1509,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1837,35 +1787,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1879,7 +1825,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1887,13 +1833,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6511,7 +6453,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6520,7 +6462,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6546,7 +6488,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), # 4: @@ -6559,7 +6501,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6602,7 +6544,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6611,7 +6553,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6634,81 +6576,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) @@ -6854,7 +6784,7 @@ def as_numeric(self): assertExpressionsEqual(self, PowExpression((self.var, 2)), e) e = obj + obj - assertExpressionsEqual(self, LinearExpression((self.mon_var, self.mon_var)), e) + assertExpressionsEqual(self, LinearExpression((self.var, self.var)), e) def test_categorize_arg_type(self): class CustomAsNumeric(NumericValue): diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 162d664e0f8..8e75ccc3feb 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -102,38 +102,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -142,7 +138,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -150,13 +146,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -196,7 +188,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -205,7 +197,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -228,7 +220,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -237,7 +229,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -264,7 +256,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -273,7 +265,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -304,7 +296,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -313,7 +305,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -339,7 +331,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -353,11 +345,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -408,37 +396,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -446,7 +418,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -454,13 +426,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -471,7 +439,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -485,11 +453,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -547,7 +511,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -561,11 +525,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -616,11 +576,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -633,7 +589,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -683,7 +639,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -697,11 +653,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -868,7 +820,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -893,7 +845,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1075,7 +1027,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1084,7 +1036,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1114,7 +1066,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1143,7 +1095,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1249,32 +1201,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1285,7 +1237,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1293,12 +1245,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1571,35 +1523,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1613,7 +1561,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1621,13 +1569,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6039,7 +5983,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6048,7 +5992,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6074,7 +6018,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), # 4: @@ -6087,7 +6031,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6130,7 +6074,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6139,7 +6083,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6162,81 +6106,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index fada7d6f6b2..12fb98d1d19 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -437,9 +437,7 @@ def test_replacement_linear_expression_with_constant(self): sub_map = dict() sub_map[id(m.x)] = 5 e2 = replace_expressions(e, sub_map) - assertExpressionsEqual( - self, e2, LinearExpression([10, MonomialTermExpression((1, m.y))]) - ) + assertExpressionsEqual(self, e2, LinearExpression([10, m.y])) e = LinearExpression(linear_coefs=[2, 3], linear_vars=[m.x, m.y]) sub_map = dict() @@ -886,20 +884,7 @@ def test_replace(self): assertExpressionsEqual( self, SumExpression( - [ - LinearExpression( - [ - MonomialTermExpression((1, m.y[1])), - MonomialTermExpression((1, m.y[2])), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.y[2])), - MonomialTermExpression((1, m.y[3])), - ] - ), - ] + [LinearExpression([m.y[1], m.y[2]]), LinearExpression([m.y[2], m.y[3]])] ) == 0, f, @@ -930,9 +915,7 @@ def test_npv_sum(self): e3 = replace_expressions(e1, {id(m.p1): m.x}) assertExpressionsEqual(self, e2, m.p2 + 2) - assertExpressionsEqual( - self, e3, LinearExpression([MonomialTermExpression((1, m.x)), 2]) - ) + assertExpressionsEqual(self, e3, LinearExpression([m.x, 2])) def test_npv_negation(self): m = ConcreteModel() diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 28025816262..5d0d6f6c21b 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -425,12 +425,7 @@ def check_two_term_disjunction_xor(self, xor, disj1, disj2): assertExpressionsEqual( self, xor.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, disj1.binary_indicator_var)), - EXPR.MonomialTermExpression((1, disj2.binary_indicator_var)), - ] - ), + EXPR.LinearExpression([disj1.binary_indicator_var, disj2.binary_indicator_var]), ) self.assertEqual(xor.lower, 1) self.assertEqual(xor.upper, 1) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 2383d4587f5..c6ac49f6d36 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -155,10 +155,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index aa846c4710a..ae2c44b899e 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -146,10 +146,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_disjunct.py b/pyomo/gdp/tests/test_disjunct.py index d969b245ee7..f93ac31fb0f 100644 --- a/pyomo/gdp/tests/test_disjunct.py +++ b/pyomo/gdp/tests/test_disjunct.py @@ -632,19 +632,13 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = m.iv + 1 - assertExpressionsEqual( - self, e, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): e = m.iv - 1 - assertExpressionsEqual( - self, - e, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -665,9 +659,7 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = 1 + m.iv - assertExpressionsEqual( - self, e, EXPR.LinearExpression([1, EXPR.MonomialTermExpression((1, m.biv))]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([1, m.biv])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -699,20 +691,14 @@ def test_cast_to_binary(self): with LoggingIntercept(out): a = m.iv a += 1 - assertExpressionsEqual( - self, a, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): a = m.iv a -= 1 - assertExpressionsEqual( - self, - a, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() From 9da87b6f0af339e116c898be6981e562a7c1d7e0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 17:37:13 -0700 Subject: [PATCH 0661/1178] Update PyROS to admit VarData in LinearExpressions --- .../contrib/pyros/pyros_algorithm_methods.py | 20 ++++++---- pyomo/contrib/pyros/tests/test_grcs.py | 40 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index f0e32a284bb..5987db074e6 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -26,6 +26,7 @@ ) from pyomo.contrib.pyros.util import get_main_elapsed_time, coefficient_matching from pyomo.core.base import value +from pyomo.core.expr import MonomialTermExpression from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.base.var import _VarData as VarData from itertools import chain @@ -69,14 +70,17 @@ def get_dr_var_to_scaled_expr_map( ssv_dr_eq_zip = zip(second_stage_vars, decision_rule_eqns) for ssv_idx, (ssv, dr_eq) in enumerate(ssv_dr_eq_zip): for term in dr_eq.body.args: - is_ssv_term = ( - isinstance(term.args[0], int) - and term.args[0] == -1 - and isinstance(term.args[1], VarData) - ) - if not is_ssv_term: - dr_var = term.args[1] - var_to_scaled_expr_map[dr_var] = term + if isinstance(term, MonomialTermExpression): + is_ssv_term = ( + isinstance(term.args[0], int) + and term.args[0] == -1 + and isinstance(term.args[1], VarData) + ) + if not is_ssv_term: + dr_var = term.args[1] + var_to_scaled_expr_map[dr_var] = term + elif isinstance(term, VarData): + var_to_scaled_expr_map[term] = MonomialTermExpression((1, term)) return var_to_scaled_expr_map diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index df3568e42a4..c308f0d6990 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -19,6 +19,7 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers +from pyomo.core.base.var import _VarData from pyomo.core.expr import ( identify_variables, identify_mutable_parameters, @@ -571,22 +572,30 @@ def test_dr_eqns_form_correct(self): dr_polynomial_terms, indexed_dr_var.values(), dr_monomial_param_combos ) for idx, (term, dr_var, param_combo) in enumerate(dr_polynomial_zip): - # term should be a monomial expression of form - # (uncertain parameter product) * (decision rule variable) - # so length of expression object should be 2 - self.assertEqual( - len(term.args), - 2, - msg=( - f"Length of `args` attribute of term {str(term)} " - f"of DR equation {dr_eq.name!r} is not as expected. " - f"Args: {term.args}" - ), - ) + # term should be either a monomial expression or scalar variable + if isinstance(term, MonomialTermExpression): + # should be of form (uncertain parameter product) * + # (decision rule variable) so length of expression + # object should be 2 + self.assertEqual( + len(term.args), + 2, + msg=( + f"Length of `args` attribute of term {str(term)} " + f"of DR equation {dr_eq.name!r} is not as expected. " + f"Args: {term.args}" + ), + ) + + # check that uncertain parameters participating in + # the monomial are as expected + param_product_multiplicand = term.args[0] + dr_var_multiplicand = term.args[1] + else: + self.assertIsInstance(term, _VarData) + param_product_multiplicand = 1 + dr_var_multiplicand = term - # check that uncertain parameters participating in - # the monomial are as expected - param_product_multiplicand = term.args[0] if idx == 0: # static DR term param_combo_found_in_term = (param_product_multiplicand,) @@ -612,7 +621,6 @@ def test_dr_eqns_form_correct(self): # check that DR variable participating in the monomial # is as expected - dr_var_multiplicand = term.args[1] self.assertIs( dr_var_multiplicand, dr_var, From 976f88df1dcd7d75e0c331da105ac6e8b8ac7282 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 21:09:54 -0700 Subject: [PATCH 0662/1178] Update doc tests to track change in LinearExpression arg types --- doc/OnlineDocs/src/expr/managing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py index 00d521d16ab..ff149e4fd5c 100644 --- a/doc/OnlineDocs/src/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -181,7 +181,7 @@ def clone_expression(expr): # x[0] + 5*x[1] print(str(ce)) # x[0] + 5*x[1] -print(e.arg(0) is not ce.arg(0)) +print(e.arg(0) is ce.arg(0)) # True print(e.arg(1) is not ce.arg(1)) # True From 1bfeebbbd91107da54c826b479e23f903c86ab21 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 8 Mar 2024 09:23:26 -0700 Subject: [PATCH 0663/1178] NFC: update docs --- pyomo/core/expr/numeric_expr.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 8ce7ee81c9a..25d83ca20f4 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1234,9 +1234,11 @@ class LinearExpression(SumExpression): """An expression object for linear polynomials. This is a derived :py:class`SumExpression` that guarantees all - arguments are either not potentially variable (e.g., native types, - Params, or NPV expressions) OR :py:class:`MonomialTermExpression` - objects. + arguments are one of the following types: + + - not potentially variable (e.g., native types, Params, or NPV expressions) + - :py:class:`MonomialTermExpression` + - :py:class:`_VarData` Args: args (tuple): Children nodes @@ -1253,7 +1255,7 @@ def __init__(self, args=None, constant=None, linear_coefs=None, linear_vars=None You can specify `args` OR (`constant`, `linear_coefs`, and `linear_vars`). If `args` is provided, it should be a list that - contains only constants, NPV objects/expressions, or + contains only constants, NPV objects/expressions, variables, or :py:class:`MonomialTermExpression` objects. Alternatively, you can specify the constant, the list of linear_coefs and the list of linear_vars separately. Note that these lists are NOT From 3879cf5ad0bc142de9ee92f15bef4af51af65b16 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 11 Mar 2024 11:37:19 -0600 Subject: [PATCH 0664/1178] Additional PyROS update to track change in LinearExpression args --- pyomo/contrib/pyros/master_problem_methods.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index abf02809396..4d2609576ff 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -398,10 +398,17 @@ def construct_dr_polishing_problem(model_data, config): all_ub_cons.append(polishing_absolute_value_ub_cons) # get monomials; ensure second-stage variable term excluded + # + # the dr_eq is a linear sum where teh first term is the + # second-stage variable: the remainder of the terms will be + # either MonomialTermExpressions or bare VarData dr_expr_terms = dr_eq.body.args[:-1] for dr_eq_term in dr_expr_terms: - dr_var_in_term = dr_eq_term.args[-1] + if dr_eq_term.is_expression_type(): + dr_var_in_term = dr_eq_term.args[-1] + else: + dr_var_in_term = dr_eq_term dr_var_in_term_idx = dr_var_in_term.index() # get corresponding polishing variable From 8f9cf83a99b5238eb17b2fd77049f6132691fb59 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 11 Mar 2024 11:42:07 -0600 Subject: [PATCH 0665/1178] NFC: fix spelling --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 4d2609576ff..8b9e85b90e9 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -399,7 +399,7 @@ def construct_dr_polishing_problem(model_data, config): # get monomials; ensure second-stage variable term excluded # - # the dr_eq is a linear sum where teh first term is the + # the dr_eq is a linear sum where the first term is the # second-stage variable: the remainder of the terms will be # either MonomialTermExpressions or bare VarData dr_expr_terms = dr_eq.body.args[:-1] From a99165c1b4c724681b94f368c1155bd317bc25c3 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 13:29:31 -0600 Subject: [PATCH 0666/1178] fix attribute error --- pyomo/util/tests/test_subsystems.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index a9b8a215fcc..05b1bf9f8f4 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -304,11 +304,11 @@ def _make_model_with_external_functions(self, named_expressions=False): m.subexpr = pyo.Expression(pyo.PositiveIntegers) subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3**2 + subexpr3 = m.subexpr[3] = subexpr2 + m.v3**2 else: subexpr1 = 2 * m.fermi(m.v1) subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[2] + m.v3**2 + subexpr3 = subexpr2 + m.v3**2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) From b3307c9abefe08a0409f51bed80f70adfd0310f6 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:29:25 -0600 Subject: [PATCH 0667/1178] check class in native_types rather than isinstance NumericValue --- pyomo/util/subsystems.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 79fbdd2d281..d497d132748 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -36,15 +36,15 @@ def initializeWalker(self, expr): return True, None def beforeChild(self, parent, child, index): - if ( + if child.__class__ in native_types: + return False, None + elif ( not self._descend_into_named_expressions - and isinstance(child, NumericValue) and child.is_named_expression_type() ): self.named_expressions.append(child) return False, None - else: - return True, None + return True, None def exitNode(self, node, data): if type(node) is ExternalFunctionExpression: From a27f90d537daa382cb67420876c2796a8282400e Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:30:05 -0600 Subject: [PATCH 0668/1178] remove unnecessary exitNode and acceptChildResult implementations --- pyomo/util/subsystems.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index d497d132748..0ed6ade756d 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -55,14 +55,6 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - # def enterNode(self, node): - # pass - - # def acceptChildResult(self, node, data, child_result, child_idx): - # if child_result.__class__ in native_types: - # return False, None - # return child_result.is_expression_type(), None - def identify_external_functions(expr): # TODO: Potentially support descend_into_named_expressions argument here. From 2ae71d7321e44c294e9ce9b0249fc1a6e514fd07 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 11 Mar 2024 14:32:48 -0600 Subject: [PATCH 0669/1178] Add clarity for specifically a single objective --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 55b013facb1..43d168a98a0 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -423,7 +423,7 @@ def _map_results(self, model, results): legacy_results.problem.number_of_variables = model.nvariables() number_of_objectives = model.nobjectives() legacy_results.problem.number_of_objectives = number_of_objectives - if number_of_objectives > 0: + if number_of_objectives == 1: obj = get_objective(model) legacy_results.problem.sense = obj.sense From 3ecd3bb80d145070e33eac81b348cf5a80eb529c Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:34:24 -0600 Subject: [PATCH 0670/1178] remove deferred todo comment --- pyomo/util/subsystems.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 0ed6ade756d..f2e2eae8444 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -57,8 +57,6 @@ def finalizeResult(self, result): def identify_external_functions(expr): - # TODO: Potentially support descend_into_named_expressions argument here. - # This will likely require converting from a generator to a function. yield from _ExternalFunctionVisitor().walk_expression(expr) From fde5edac7cb378d5d06bd1e7b929f70597155981 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:36:42 -0600 Subject: [PATCH 0671/1178] remove HierarchicalTimer use from subsystem calls --- pyomo/util/subsystems.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index f2e2eae8444..5789829ac54 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -101,12 +101,7 @@ def add_local_external_functions(block): return fcn_comp_map -from pyomo.common.timing import HierarchicalTimer - - -def create_subsystem_block( - constraints, variables=None, include_fixed=False, timer=None -): +def create_subsystem_block(constraints, variables=None, include_fixed=False): """This function creates a block to serve as a subsystem with the specified variables and constraints. To satisfy certain writers, other variables that appear in the constraints must be added to the block as @@ -130,36 +125,24 @@ def create_subsystem_block( as well as other variables present in the constraints """ - if timer is None: - timer = HierarchicalTimer() if variables is None: variables = [] - timer.start("block") block = Block(concrete=True) - timer.stop("block") - timer.start("reference") block.vars = Reference(variables) block.cons = Reference(constraints) - timer.stop("reference") var_set = ComponentSet(variables) input_vars = [] - timer.start("identify-vars") for con in constraints: for var in identify_variables(con.expr, include_fixed=include_fixed): if var not in var_set: input_vars.append(var) var_set.add(var) - timer.stop("identify-vars") - timer.start("reference") block.input_vars = Reference(input_vars) - timer.stop("reference") - timer.start("external-fcns") add_local_external_functions(block) - timer.stop("external-fcns") return block -def generate_subsystem_blocks(subsystems, include_fixed=False, timer=None): +def generate_subsystem_blocks(subsystems, include_fixed=False): """Generates blocks that contain subsystems of variables and constraints. Arguments @@ -178,10 +161,8 @@ def generate_subsystem_blocks(subsystems, include_fixed=False, timer=None): not specified are contained in the input_vars component. """ - if timer is None: - timer = HierarchicalTimer() for cons, vars in subsystems: - block = create_subsystem_block(cons, vars, include_fixed, timer=timer) + block = create_subsystem_block(cons, vars, include_fixed) yield block, list(block.input_vars.values()) From 40debe7f52d4370440824f43ef26c550a30f8713 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:40:32 -0600 Subject: [PATCH 0672/1178] remove hierarchical timer usage from scc_solver module --- .../contrib/incidence_analysis/scc_solver.py | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index eff4f5ae5fa..8c38333e058 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -11,7 +11,6 @@ import logging -from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks @@ -26,7 +25,7 @@ def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False, igraph=None, timer=None + constraints, variables=None, include_fixed=False, igraph=None ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization @@ -58,10 +57,7 @@ def generate_strongly_connected_components( "input variables" for that block. """ - if timer is None: - timer = HierarchicalTimer() if variables is None: - timer.start("generate-vars") variables = list( _generate_variables_in_constraints( constraints, @@ -69,30 +65,23 @@ def generate_strongly_connected_components( method=IncidenceMethod.ampl_repn, ) ) - timer.stop("generate-vars") assert len(variables) == len(constraints) if igraph is None: igraph = IncidenceGraphInterface() - timer.start("block-triang") var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) - timer.stop("block-triang") subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] - timer.start("generate-block") for block, inputs in generate_subsystem_blocks( - subsets, include_fixed=include_fixed, timer=timer + subsets, include_fixed=include_fixed ): - timer.stop("generate-block") # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) # Note that this code, after the last yield, I believe is only called # at time of GC. - timer.start("generate-block") - timer.stop("generate-block") def solve_strongly_connected_components( @@ -102,7 +91,6 @@ def solve_strongly_connected_components( solve_kwds=None, use_calc_var=True, calc_var_kwds=None, - timer=None, ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -142,10 +130,7 @@ def solve_strongly_connected_components( solve_kwds = {} if calc_var_kwds is None: calc_var_kwds = {} - if timer is None: - timer = HierarchicalTimer() - timer.start("igraph") igraph = IncidenceGraphInterface( block, active=True, @@ -153,27 +138,22 @@ def solve_strongly_connected_components( include_inequality=False, method=IncidenceMethod.ampl_repn, ) - timer.stop("igraph") constraints = igraph.constraints variables = igraph.variables res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) - timer.start("generate-scc") for scc, inputs in generate_strongly_connected_components( - constraints, variables, timer=timer, igraph=igraph + constraints, variables, igraph=igraph ): - timer.stop("generate-scc") with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) if N == 1 and use_calc_var: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") - timer.start("calc-var-from-con") results = calculate_variable_from_constraint( scc.vars[0], scc.cons[0], **calc_var_kwds ) - timer.stop("calc-var-from-con") else: if solver is None: var_names = [var.name for var in scc.vars.values()][:10] @@ -186,10 +166,6 @@ def solve_strongly_connected_components( ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") - timer.start("scc-subsolver") results = solver.solve(scc, **solve_kwds) - timer.stop("scc-subsolver") res_list.append(results) - timer.start("generate-scc") - timer.stop("generate-scc") return res_list From e9e63a00da4014195b6d82549f9f48f034d447b7 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 15:28:58 -0600 Subject: [PATCH 0673/1178] use new variable visitor in get_vars_from_components rather than identify_variables_in_expressions --- pyomo/core/expr/visitor.py | 34 ----------------------------- pyomo/util/vars_from_expressions.py | 31 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 51864044396..bccf0eda899 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1433,40 +1433,6 @@ def acceptChildResult(self, node, data, child_result, child_idx): return child_result.is_expression_type(), None -def identify_variables_in_components(components, include_fixed=True): - visitor = _StreamVariableVisitor( - include_fixed=include_fixed, descend_into_named_expressions=False - ) - all_variables = [] - for comp in components: - all_variables.extend(visitor.walk_expressions(comp.expr)) - - named_expr_set = set() - unique_named_exprs = [] - for expr in visitor.named_expressions: - if id(expr) in named_expr_set: - named_expr_set.add(id(expr)) - unique_named_exprs.append(expr) - - while unique_named_exprs: - expr = unique_named_exprs.pop() - visitor.named_expressions.clear() - all_variables.extend(visitor.walk_expression(expr.expr)) - - for new_expr in visitor.named_expressions: - if id(new_expr) not in named_expr_set: - named_expr_set.add(new_expr) - unique_named_exprs.append(new_expr) - - unique_vars = [] - var_set = set() - for var in all_variables: - if id(var) not in var_set: - var_set.add(id(var)) - unique_vars.append(var) - return unique_vars - - def identify_variables(expr, include_fixed=True): """ A generator that yields a sequence of variables diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index f9b3f1ab8ae..1fe614273ab 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -17,7 +17,7 @@ actually in the subtree or not. """ from pyomo.core import Block -import pyomo.core.expr as EXPR +from pyomo.core.expr.visitor import _StreamVariableVisitor def get_vars_from_components( @@ -42,7 +42,10 @@ def get_vars_from_components( descend_into: Ctypes to descend into when finding Constraints descent_order: Traversal strategy for finding the objects of type ctype """ - seen = set() + visitor = _StreamVariableVisitor( + include_fixed=include_fixed, descend_into_named_expressions=False + ) + variables = [] for constraint in block.component_data_objects( ctype, active=active, @@ -50,9 +53,21 @@ def get_vars_from_components( descend_into=descend_into, descent_order=descent_order, ): - for var in EXPR.identify_variables( - constraint.expr, include_fixed=include_fixed - ): - if id(var) not in seen: - seen.add(id(var)) - yield var + variables.extend(visitor.walk_expression(constraint.expr)) + seen_named_exprs = set() + named_expr_stack = list(visitor.named_expressions) + while named_expr_stack: + expr = named_expr_stack.pop() + # Clear visitor's named expression cache so we only identify new + # named expressions + visitor.named_expressions.clear() + variables.extend(visitor.walk_expression(expr.expr)) + for new_expr in visitor.named_expressions: + if id(new_expr) not in seen_named_exprs: + seen_named_exprs.add(id(new_expr)) + named_expr_stack.append(new_expr) + seen = set() + for var in variables: + if id(var) not in seen: + seen.add(id(var)) + yield var From 38c78a329b282b12a50be1df5055f2d1b6978a59 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 15:31:24 -0600 Subject: [PATCH 0674/1178] remove unnecessary walker callbacks --- pyomo/core/expr/visitor.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index bccf0eda899..3c1e486d38f 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1424,14 +1424,6 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._variables - def enterNode(self, node): - pass - - def acceptChildResult(self, node, data, child_result, child_idx): - if child_result.__class__ in native_types: - return False, None - return child_result.is_expression_type(), None - def identify_variables(expr, include_fixed=True): """ From 0e9af931d9e4149728bc192bddc0aaf65d772168 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 18:03:43 -0600 Subject: [PATCH 0675/1178] fix typos in test --- pyomo/util/tests/test_subsystems.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index 05b1bf9f8f4..fe093b4723f 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -341,11 +341,11 @@ def test_identify_external_functions(self): @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") def test_local_external_functions_with_named_expressions(self): m = self._make_model_with_external_functions(named_expressions=True) - variables = list(pyo.component_data_objects(pyo.Var)) - constraints = list(pyo.component_data_objects(pyo.Constraint, active=True)) + variables = list(m.component_data_objects(pyo.Var)) + constraints = list(m.component_data_objects(pyo.Constraint, active=True)) b = create_subsystem_block(constraints, variables) - self.assertTrue(isinstance(m._gsl_sf_bessel_J0, pyo.ExternalFunction)) - self.assertTrue(isinstance(m._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) + self.assertTrue(isinstance(b._gsl_sf_bessel_J0, pyo.ExternalFunction)) + self.assertTrue(isinstance(b._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) def _solve_ef_model_with_ipopt(self): m = self._make_model_with_external_functions() From 016d6d1b4536e6147984e792a23dbb0590dc5439 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 18:06:44 -0600 Subject: [PATCH 0676/1178] function args on single line --- pyomo/contrib/incidence_analysis/scc_solver.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 8c38333e058..aa59b698ce9 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -85,12 +85,7 @@ def generate_strongly_connected_components( def solve_strongly_connected_components( - block, - *, - solver=None, - solve_kwds=None, - use_calc_var=True, - calc_var_kwds=None, + block, *, solver=None, solve_kwds=None, use_calc_var=True, calc_var_kwds=None ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. From f32bd2e26ca0f2ecc6ea8d883b40851a4dcd2a4a Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 18:15:38 -0600 Subject: [PATCH 0677/1178] method args on single line --- pyomo/core/expr/visitor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 3c1e486d38f..3d5608bda4c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1389,11 +1389,7 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): - def __init__( - self, - include_fixed=False, - descend_into_named_expressions=True, - ): + def __init__(self, include_fixed=False, descend_into_named_expressions=True): self._include_fixed = include_fixed self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] From 87644ca8d4d9d31e05809b0492ade63b90e8c184 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 19:42:51 -0600 Subject: [PATCH 0678/1178] update condition for skipping named expression in variable visitor --- pyomo/core/expr/visitor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 3d5608bda4c..b31fa20f77c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1401,9 +1401,10 @@ def initializeWalker(self, expr): return True, None def beforeChild(self, parent, child, index): - if ( + if child.__class__ in native_types: + return False, None + elif ( not self._descend_into_named_expressions - and isinstance(child, NumericValue) and child.is_named_expression_type() ): self.named_expressions.append(child) From e6e7259a258d560d633e8b2dcf5d93ae406d2337 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 19:47:41 -0600 Subject: [PATCH 0679/1178] super.__init__ call in variable visitor --- pyomo/core/expr/visitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index b31fa20f77c..befdef0be71 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1390,6 +1390,7 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): def __init__(self, include_fixed=False, descend_into_named_expressions=True): + super().__init__() self._include_fixed = include_fixed self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] From 24644ff06414f689937551f204eea7fc81cef7b2 Mon Sep 17 00:00:00 2001 From: robbybp Date: Tue, 12 Mar 2024 10:30:19 -0600 Subject: [PATCH 0680/1178] remove outdated comment --- pyomo/contrib/incidence_analysis/scc_solver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index aa59b698ce9..0c59fe8703e 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -80,8 +80,6 @@ def generate_strongly_connected_components( # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) - # Note that this code, after the last yield, I believe is only called - # at time of GC. def solve_strongly_connected_components( From d12d69b4b1a409ef7450ae5f22229d19d3401dc1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 12 Mar 2024 13:39:55 -0600 Subject: [PATCH 0681/1178] Temporarily pinning to highspy pre-release for testing --- .github/workflows/test_pr_and_main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..a2060240391 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -605,7 +605,8 @@ jobs: if: ${{ ! matrix.slim }} shell: bash run: | - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy \ + echo "NOTE: temporarily pinning to highspy pre-release for testing" + $PYTHON_EXE -m pip install --cache-dir cache/pip highspy==1.7.1.dev1 \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking From de294ee874602cf1c94615dfa2a49ed162439464 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Mar 2024 15:39:12 -0600 Subject: [PATCH 0682/1178] fix variable assignment --- pyomo/util/tests/test_subsystems.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index fe093b4723f..089888bd6a9 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -302,9 +302,12 @@ def _make_model_with_external_functions(self, named_expressions=False): m.v3 = pyo.Var(initialize=3.0) if named_expressions: m.subexpr = pyo.Expression(pyo.PositiveIntegers) - subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) - subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[3] = subexpr2 + m.v3**2 + m.subexpr[1] = 2 * m.fermi(m.v1) + m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) + m.subexpr[3] = m.subexpr[2] + m.v3**2 + subexpr1 = m.subexpr[1] + subexpr2 = m.subexpr[2] + subexpr3 = m.subexpr[3] else: subexpr1 = 2 * m.fermi(m.v1) subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) From 8371c8ae548aa57e80ec55fa69a3d4d7220bebb0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 12 Mar 2024 16:18:57 -0600 Subject: [PATCH 0683/1178] Catch when NLv2 presolve identifies/removes independent linear subsystems --- pyomo/repn/plugins/nl_writer.py | 32 ++++++++++++- pyomo/repn/tests/ampl/test_nlv2.py | 72 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index b82d4df77e2..2db3e66cbb6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1081,9 +1081,37 @@ def write(self, model): # Update any eliminated variables to point to the (potentially # scaled) substituted variables - for _id, expr_info in eliminated_vars.items(): + for _id, expr_info in list(eliminated_vars.items()): nl, args, _ = expr_info.compile_repn(visitor) - _vmap[_id] = nl.rstrip() % tuple(_vmap[_id] for _id in args) + for _i in args: + # It is possible that the eliminated variable could + # reference another variable that is no longer part of + # the model and therefore does not have a _vmap entry. + # This can happen when there is an underdetermined + # independent linear subsystem and the presolve removed + # all the constraints from the subsystem. Because the + # free variables in the subsystem are not referenced + # anywhere else in the model, they are not part of the + # `varaibles` list. Implicitly "fix" it to an arbitrary + # valid value from the presolved domain (see #3192). + if _i not in _vmap: + lb, ub = var_bounds[_i] + if lb is None: + lb = -inf + if ub is None: + ub = inf + if lb <= 0 <= ub: + val = 0 + else: + val = lb if abs(lb) < abs(ub) else ub + eliminated_vars[_i] = AMPLRepn(val, {}, None) + _vmap[_i] = expr_info.compile_repn(visitor)[0] + logger.warning( + "presolve identified an underdetermined independent " + "linear subsystem that was removed from the model. " + f"Setting '{var_map[_i]}' == {val}" + ) + _vmap[_id] = nl.rstrip() % tuple(_vmap[_i] for _i in args) r_lines = [None] * n_cons for idx, (con, expr_info, lb, ub) in enumerate(constraints): diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 86eb43d9a37..be72025edcd 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1812,6 +1812,78 @@ def test_presolve_zero_coef(self): ) ) + def test_presolve_independent_subsystem(self): + # This is derived from the example in #3192 + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.d = Constraint(expr=m.z == m.y) + m.c = Constraint(expr=m.y == m.x) + m.o = Objective(expr=0) + + ref = """g3 1 1 0 #problem unknown + 0 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 0 #nonzeros in Jacobian, obj. gradient + 1 0 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #o +n0 +x0 #initial guess +r #0 ranges (rhs's) +b #0 bounds (on variables) +k-1 #intermediate Jacobian column lengths +""" + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == 0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + m.x.lb = 5.0 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == 5.0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + m.x.lb = -5.0 + m.z.ub = -2.0 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == -2.0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + def test_scaling(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=0) From b3a4b06e9bd1cfb3c3cecc9a4b1a85c262cfbdd3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 12 Mar 2024 16:23:05 -0600 Subject: [PATCH 0684/1178] NFC: fix typo --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 2db3e66cbb6..ee5b65149ae 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1092,7 +1092,7 @@ def write(self, model): # all the constraints from the subsystem. Because the # free variables in the subsystem are not referenced # anywhere else in the model, they are not part of the - # `varaibles` list. Implicitly "fix" it to an arbitrary + # `variables` list. Implicitly "fix" it to an arbitrary # valid value from the presolved domain (see #3192). if _i not in _vmap: lb, ub = var_bounds[_i] From 440bf86c9cb9fabff38daa5d27cbdd95d18be1f4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 13 Mar 2024 13:12:58 -0600 Subject: [PATCH 0685/1178] Minor logic reordering to make the intent more clear --- pyomo/repn/util.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index a51ee1c6d64..1b3056738bf 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -400,14 +400,15 @@ def __init__(self, *args, **kwargs): def __missing__(self, key): if type(key) is tuple: - node_class = key[0] - node_args = key[1:] # Only lookup/cache argument-specific handlers for unary, # binary and ternary operators - if len(key) > 3: - key = node_class - if key in self: - return self[key] + if len(key) <= 3: + node_class = key[0] + node_args = key[1:] + else: + node_class = key = key[0] + if node_class in self: + return self[node_class] else: node_class = key bases = node_class.__mro__ @@ -446,7 +447,7 @@ def __missing__(self, key): def unexpected_expression_type(self, visitor, node, *args): raise DeveloperError( f"Unexpected expression node type '{type(node).__name__}' " - f"found while walking expression tree in {type(self).__name__}." + f"found while walking expression tree in {type(visitor).__name__}." ) From e1fa2569252d849884da0569c95f8011716d3507 Mon Sep 17 00:00:00 2001 From: robbybp Date: Wed, 13 Mar 2024 23:06:44 -0600 Subject: [PATCH 0686/1178] [WIP] initial attempt at implementing identify_variables with a named expression cache --- pyomo/core/expr/visitor.py | 105 +++++++++++++++++++++------- pyomo/util/vars_from_expressions.py | 63 +++++++++++------ 2 files changed, 122 insertions(+), 46 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index befdef0be71..20d8d72fcbb 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1389,12 +1389,20 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): - def __init__(self, include_fixed=False, descend_into_named_expressions=True): + def __init__( + self, + include_fixed=False, + #descend_into_named_expressions=True, + named_expression_cache=None, + ): super().__init__() self._include_fixed = include_fixed - self._descend_into_named_expressions = descend_into_named_expressions + #self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] - # Should we allow re-use of this visitor for multiple expressions? + if named_expression_cache is None: + named_expression_cache = {} + self._named_expression_cache = named_expression_cache + self._active_named_expressions = [] def initializeWalker(self, expr): self._variables = [] @@ -1404,12 +1412,26 @@ def initializeWalker(self, expr): def beforeChild(self, parent, child, index): if child.__class__ in native_types: return False, None - elif ( - not self._descend_into_named_expressions - and child.is_named_expression_type() - ): - self.named_expressions.append(child) - return False, None + #elif ( + # not self._descend_into_named_expressions + # and child.is_named_expression_type() + #): + # self.named_expressions.append(child) + # return False, None + elif child.is_named_expression_type(): + if id(child) in self._named_expression_cache: + # We have already encountered this named expression. We just add + # the cached variables to our list and don't descend. + for var in self._named_expression_cache[id(child)][0]: + if id(var) not in self._seen: + self._variables.append(var) + return False, None + else: + # If we are descending into a new named expression, initialize + # a cache to store the expression's local variables. + self._named_expression_cache[id(child)] = ([], set()) + self._active_named_expressions.append(id(child)) + return True, None else: return True, None @@ -1418,12 +1440,35 @@ def exitNode(self, node, data): if id(node) not in self._seen: self._seen.add(id(node)) self._variables.append(node) + if self._active_named_expressions: + # If we are in a named expression, add new variables to the cache. + eid = self._active_named_expressions[-1] + local_vars, local_var_set = self._named_expression_cache[eid] + if id(node) not in local_var_set: + local_var_set.add(id(node)) + local_vars.append(node) + elif node.is_named_expression_type(): + # If we are returning from a named expression, we have at least one + # active named expression. + eid = self._active_named_expressions.pop() + if self._active_named_expressions: + # If we still are in a named expression, we update that expression's + # cache with any new variables encountered. + new_eid = self._active_named_expressions[-1] + old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] + new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] + + for var in old_expr_vars: + if id(var) not in new_expr_var_set: + new_expr_var_set.add(id(var)) + new_expr_vars.append(var) def finalizeResult(self, result): return self._variables -def identify_variables(expr, include_fixed=True): +# TODO: descend_into_named_expressions option? +def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ A generator that yields a sequence of variables in an expression tree. @@ -1437,22 +1482,34 @@ def identify_variables(expr, include_fixed=True): Yields: Each variable that is found. """ - visitor = _VariableVisitor() - if include_fixed: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - yield from v - else: - yield v + if named_expression_cache is None: + named_expression_cache = {} + + NEW = True + if NEW: + visitor = _StreamVariableVisitor( + named_expression_cache=named_expression_cache, + include_fixed=False, + ) + variables = visitor.walk_expression(expr) + yield from variables else: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - for v_i in v: - if not v_i.is_fixed(): - yield v_i - else: - if not v.is_fixed(): + visitor = _VariableVisitor() + if include_fixed: + for v in visitor.xbfs_yield_leaves(expr): + if isinstance(v, tuple): + yield from v + else: yield v + else: + for v in visitor.xbfs_yield_leaves(expr): + if isinstance(v, tuple): + for v_i in v: + if not v_i.is_fixed(): + yield v_i + else: + if not v.is_fixed(): + yield v # ===================================================== diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 1fe614273ab..22e6e6dab8d 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -18,6 +18,7 @@ """ from pyomo.core import Block from pyomo.core.expr.visitor import _StreamVariableVisitor +from pyomo.core.expr import identify_variables def get_vars_from_components( @@ -42,10 +43,38 @@ def get_vars_from_components( descend_into: Ctypes to descend into when finding Constraints descent_order: Traversal strategy for finding the objects of type ctype """ - visitor = _StreamVariableVisitor( - include_fixed=include_fixed, descend_into_named_expressions=False - ) - variables = [] + #visitor = _StreamVariableVisitor( + # include_fixed=include_fixed, descend_into_named_expressions=False + #) + #variables = [] + #for constraint in block.component_data_objects( + # ctype, + # active=active, + # sort=sort, + # descend_into=descend_into, + # descent_order=descent_order, + #): + # variables.extend(visitor.walk_expression(constraint.expr)) + # seen_named_exprs = set() + # named_expr_stack = list(visitor.named_expressions) + # while named_expr_stack: + # expr = named_expr_stack.pop() + # # Clear visitor's named expression cache so we only identify new + # # named expressions + # visitor.named_expressions.clear() + # variables.extend(visitor.walk_expression(expr.expr)) + # for new_expr in visitor.named_expressions: + # if id(new_expr) not in seen_named_exprs: + # seen_named_exprs.add(id(new_expr)) + # named_expr_stack.append(new_expr) + #seen = set() + #for var in variables: + # if id(var) not in seen: + # seen.add(id(var)) + # yield var + + seen = set() + named_expression_cache = {} for constraint in block.component_data_objects( ctype, active=active, @@ -53,21 +82,11 @@ def get_vars_from_components( descend_into=descend_into, descent_order=descent_order, ): - variables.extend(visitor.walk_expression(constraint.expr)) - seen_named_exprs = set() - named_expr_stack = list(visitor.named_expressions) - while named_expr_stack: - expr = named_expr_stack.pop() - # Clear visitor's named expression cache so we only identify new - # named expressions - visitor.named_expressions.clear() - variables.extend(visitor.walk_expression(expr.expr)) - for new_expr in visitor.named_expressions: - if id(new_expr) not in seen_named_exprs: - seen_named_exprs.add(id(new_expr)) - named_expr_stack.append(new_expr) - seen = set() - for var in variables: - if id(var) not in seen: - seen.add(id(var)) - yield var + for var in identify_variables( + constraint.expr, + include_fixed=include_fixed, + named_expression_cache=named_expression_cache, + ): + if id(var) not in seen: + seen.add(id(var)) + yield var From 53899315af8ba53ae0c0e22851b7d7894a03ed33 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 14 Mar 2024 10:19:33 -0600 Subject: [PATCH 0687/1178] Add link to the companion notebooks for Hands-on Mathematival Optimization with Python --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95558e52a42..12c3ce8ed9a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ version, we will remove testing for that Python version. * [Pyomo Workshop Slides](https://github.com/Pyomo/pyomo-tutorials/blob/main/Pyomo-Workshop-December-2023.pdf) * [Prof. Jeffrey Kantor's Pyomo Cookbook](https://jckantor.github.io/ND-Pyomo-Cookbook/) +* The [companion notebooks](https://mobook.github.io/MO-book/intro.html) + for *Hands-On Mathematical Optimization with Python* * [Pyomo Gallery](https://github.com/Pyomo/PyomoGallery) ### Getting Help From cca02a124c4d39fe38ff1e5fe749c089d9cc5f27 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 14 Mar 2024 10:26:48 -0600 Subject: [PATCH 0688/1178] Syncing tutorials/examples list between README and RTD --- doc/OnlineDocs/tutorial_examples.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/tutorial_examples.rst b/doc/OnlineDocs/tutorial_examples.rst index dc58b6a6f59..6a40949ef90 100644 --- a/doc/OnlineDocs/tutorial_examples.rst +++ b/doc/OnlineDocs/tutorial_examples.rst @@ -9,7 +9,9 @@ Additional Pyomo tutorials and examples can be found at the following links: `Prof. Jeffrey Kantor's Pyomo Cookbook `_ -`Pyomo Gallery -`_ +The `companion notebooks `_ +for *Hands-On Mathematical Optimization with Python* + +`Pyomo Gallery `_ From 5ea6ae75c4f92c6b66fd4c839c2d1a8825bb1a39 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 16:56:44 -0600 Subject: [PATCH 0689/1178] potentially working implementation of identify-variables with efficient named expression handling --- pyomo/core/expr/visitor.py | 88 ++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 20d8d72fcbb..7ae1900f9b8 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1405,26 +1405,46 @@ def __init__( self._active_named_expressions = [] def initializeWalker(self, expr): - self._variables = [] - self._seen = set() - return True, None + if expr.__class__ in native_types: + return False, [] + elif expr.is_named_expression_type(): + eid = id(expr) + if eid in self._named_expression_cache: + variables, var_set = self._named_expression_cache[eid] + return False, variables + else: + self._variables = [] + self._seen = set() + self._named_expression_cache[eid] = [], set() + self._active_named_expressions.append(eid) + return True, expr + else: + self._variables = [] + self._seen = set() + return True, expr def beforeChild(self, parent, child, index): if child.__class__ in native_types: return False, None - #elif ( - # not self._descend_into_named_expressions - # and child.is_named_expression_type() - #): - # self.named_expressions.append(child) - # return False, None elif child.is_named_expression_type(): - if id(child) in self._named_expression_cache: + eid = id(child) + if eid in self._named_expression_cache: # We have already encountered this named expression. We just add # the cached variables to our list and don't descend. - for var in self._named_expression_cache[id(child)][0]: - if id(var) not in self._seen: - self._variables.append(var) + if self._active_named_expressions: + # If we are in another named expression, we update the + # parent expression's cache + parent_eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[parent_eid] + else: + # If we are not in a named expression, we update the global + # list + variables = self._variables + var_set = self._seen + for var in self._named_expression_cache[eid][0]: + if id(var) not in var_set: + var_set.add(id(var)) + variables.append(var) return False, None else: # If we are descending into a new named expression, initialize @@ -1432,6 +1452,18 @@ def beforeChild(self, parent, child, index): self._named_expression_cache[id(child)] = ([], set()) self._active_named_expressions.append(id(child)) return True, None + elif child.is_variable_type() and (self._include_fixed or not child.fixed): + if id(child) not in self._seen: + self._seen.add(id(child)) + self._variables.append(child) + if self._active_named_expressions: + # If we are in a named expression, add new variables to the cache. + eid = self._active_named_expressions[-1] + local_vars, local_var_set = self._named_expression_cache[eid] + if id(child) not in local_var_set: + local_var_set.add(id(child)) + local_vars.append(child) + return False, None else: return True, None @@ -1449,19 +1481,29 @@ def exitNode(self, node, data): local_vars.append(node) elif node.is_named_expression_type(): # If we are returning from a named expression, we have at least one - # active named expression. + # active named expression. We must make sure that we properly + # handle the variables for the named expression we just exited. eid = self._active_named_expressions.pop() if self._active_named_expressions: # If we still are in a named expression, we update that expression's # cache with any new variables encountered. - new_eid = self._active_named_expressions[-1] - old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] - new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] - - for var in old_expr_vars: - if id(var) not in new_expr_var_set: - new_expr_var_set.add(id(var)) - new_expr_vars.append(var) + #new_eid = self._active_named_expressions[-1] + #old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] + #new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] + + #for var in old_expr_vars: + # if id(var) not in new_expr_var_set: + # new_expr_var_set.add(id(var)) + # new_expr_vars.append(var) + parent_eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[parent_eid] + else: + variables = self._variables + var_set = self._seen + for var in self._named_expression_cache[eid][0]: + if id(var) not in var_set: + var_set.add(id(var)) + variables.append(var) def finalizeResult(self, result): return self._variables @@ -1489,7 +1531,7 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): if NEW: visitor = _StreamVariableVisitor( named_expression_cache=named_expression_cache, - include_fixed=False, + include_fixed=include_fixed, ) variables = visitor.walk_expression(expr) yield from variables From a82c7509ade2c6fb65f69c3b38472ee81ce6519b Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:21:02 -0600 Subject: [PATCH 0690/1178] update identify_variables tests to use ComponentSet to not rely on variable order --- pyomo/core/tests/unit/test_visitor.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 12fb98d1d19..d6d83f84e67 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -145,7 +145,8 @@ def test_identify_vars_vars(self): self.assertEqual(list(identify_variables(m.a + m.b[1])), [m.a, m.b[1]]) self.assertEqual(list(identify_variables(m.a ** m.b[1])), [m.a, m.b[1]]) self.assertEqual( - list(identify_variables(m.a ** m.b[1] + m.b[2])), [m.b[2], m.a, m.b[1]] + ComponentSet(identify_variables(m.a ** m.b[1] + m.b[2])), + ComponentSet([m.b[2], m.a, m.b[1]]), ) self.assertEqual( list(identify_variables(m.a ** m.b[1] + m.b[2] * m.b[3] * m.b[2])), @@ -159,14 +160,20 @@ def test_identify_vars_vars(self): # Identify variables in the arguments to functions # self.assertEqual( - list(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), - [m.b[1], m.a], + ComponentSet(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), + ComponentSet([m.b[1], m.a]), ) self.assertEqual( list(identify_variables(m.x(m.p, 'string_param', 1, []) * m.b[1])), [m.b[1]] ) - self.assertEqual(list(identify_variables(tanh(m.a) * m.b[1])), [m.b[1], m.a]) - self.assertEqual(list(identify_variables(abs(m.a) * m.b[1])), [m.b[1], m.a]) + self.assertEqual( + ComponentSet(identify_variables(tanh(m.a) * m.b[1])), + ComponentSet([m.b[1], m.a]), + ) + self.assertEqual( + ComponentSet(identify_variables(abs(m.a) * m.b[1])), + ComponentSet([m.b[1], m.a]), + ) # # Check logic for allowing duplicates # From f8299f6bf6526b20d5085b34e44ea7321a668e43 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:25:36 -0600 Subject: [PATCH 0691/1178] remove commented code and old identify_variables implementation --- pyomo/core/expr/visitor.py | 43 ++++++-------------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 7ae1900f9b8..b284c7fa38f 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1392,12 +1392,10 @@ class _StreamVariableVisitor(StreamBasedExpressionVisitor): def __init__( self, include_fixed=False, - #descend_into_named_expressions=True, named_expression_cache=None, ): super().__init__() self._include_fixed = include_fixed - #self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] if named_expression_cache is None: named_expression_cache = {} @@ -1487,14 +1485,6 @@ def exitNode(self, node, data): if self._active_named_expressions: # If we still are in a named expression, we update that expression's # cache with any new variables encountered. - #new_eid = self._active_named_expressions[-1] - #old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] - #new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] - - #for var in old_expr_vars: - # if id(var) not in new_expr_var_set: - # new_expr_var_set.add(id(var)) - # new_expr_vars.append(var) parent_eid = self._active_named_expressions[-1] variables, var_set = self._named_expression_cache[parent_eid] else: @@ -1509,7 +1499,6 @@ def finalizeResult(self, result): return self._variables -# TODO: descend_into_named_expressions option? def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ A generator that yields a sequence of variables @@ -1526,32 +1515,12 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ if named_expression_cache is None: named_expression_cache = {} - - NEW = True - if NEW: - visitor = _StreamVariableVisitor( - named_expression_cache=named_expression_cache, - include_fixed=include_fixed, - ) - variables = visitor.walk_expression(expr) - yield from variables - else: - visitor = _VariableVisitor() - if include_fixed: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - yield from v - else: - yield v - else: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - for v_i in v: - if not v_i.is_fixed(): - yield v_i - else: - if not v.is_fixed(): - yield v + visitor = _StreamVariableVisitor( + named_expression_cache=named_expression_cache, + include_fixed=include_fixed, + ) + variables = visitor.walk_expression(expr) + yield from variables # ===================================================== From d232fcab6a66add09f746febb706947df9f534ff Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:27:35 -0600 Subject: [PATCH 0692/1178] handle variable at root in initializeWalker rather than exitNode --- pyomo/core/expr/visitor.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index b284c7fa38f..4cdc77df41e 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1416,6 +1416,8 @@ def initializeWalker(self, expr): self._named_expression_cache[eid] = [], set() self._active_named_expressions.append(eid) return True, expr + elif expr.is_variable_type(): + return False, [expr] else: self._variables = [] self._seen = set() @@ -1466,18 +1468,7 @@ def beforeChild(self, parent, child, index): return True, None def exitNode(self, node, data): - if node.is_variable_type() and (self._include_fixed or not node.fixed): - if id(node) not in self._seen: - self._seen.add(id(node)) - self._variables.append(node) - if self._active_named_expressions: - # If we are in a named expression, add new variables to the cache. - eid = self._active_named_expressions[-1] - local_vars, local_var_set = self._named_expression_cache[eid] - if id(node) not in local_var_set: - local_var_set.add(id(node)) - local_vars.append(node) - elif node.is_named_expression_type(): + if node.is_named_expression_type(): # If we are returning from a named expression, we have at least one # active named expression. We must make sure that we properly # handle the variables for the named expression we just exited. From 796ccef2e5c69a104ef3a7e9e307e5c323a26907 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:28:21 -0600 Subject: [PATCH 0693/1178] remove previous vars_from_expressions implementation --- pyomo/util/vars_from_expressions.py | 30 ----------------------------- 1 file changed, 30 deletions(-) diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 22e6e6dab8d..62953af456b 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -43,36 +43,6 @@ def get_vars_from_components( descend_into: Ctypes to descend into when finding Constraints descent_order: Traversal strategy for finding the objects of type ctype """ - #visitor = _StreamVariableVisitor( - # include_fixed=include_fixed, descend_into_named_expressions=False - #) - #variables = [] - #for constraint in block.component_data_objects( - # ctype, - # active=active, - # sort=sort, - # descend_into=descend_into, - # descent_order=descent_order, - #): - # variables.extend(visitor.walk_expression(constraint.expr)) - # seen_named_exprs = set() - # named_expr_stack = list(visitor.named_expressions) - # while named_expr_stack: - # expr = named_expr_stack.pop() - # # Clear visitor's named expression cache so we only identify new - # # named expressions - # visitor.named_expressions.clear() - # variables.extend(visitor.walk_expression(expr.expr)) - # for new_expr in visitor.named_expressions: - # if id(new_expr) not in seen_named_exprs: - # seen_named_exprs.add(id(new_expr)) - # named_expr_stack.append(new_expr) - #seen = set() - #for var in variables: - # if id(var) not in seen: - # seen.add(id(var)) - # yield var - seen = set() named_expression_cache = {} for constraint in block.component_data_objects( From 7c130ef1b6d62b2f1a2ffdf6ae399dbb8fad047a Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:38:43 -0600 Subject: [PATCH 0694/1178] add docstring and comments to _StreamVariableVisitor --- pyomo/core/expr/visitor.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 4cdc77df41e..6dd587cf2d4 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1394,12 +1394,25 @@ def __init__( include_fixed=False, named_expression_cache=None, ): + """Visitor that collects all unique variables participating in an + expression + + Args: + include_fixed (bool): Whether to include fixed variables + named_expression_cache (optional, dict): Dict mapping ids of named + expressions to a tuple of the list of all variables and the + set of all variable ids contained in the named expression. + + """ super().__init__() self._include_fixed = include_fixed - self.named_expressions = [] if named_expression_cache is None: + # This cache will map named expression ids to the + # tuple: ([variables], {variable ids}) named_expression_cache = {} self._named_expression_cache = named_expression_cache + # Stack of active named expressions. This holds the id of + # expressions we are currently in. self._active_named_expressions = [] def initializeWalker(self, expr): From 0e3015dcf9e2daece51a485868537f9be8443e03 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:44:10 -0600 Subject: [PATCH 0695/1178] arguments on single line --- pyomo/core/expr/visitor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 6dd587cf2d4..1cd2ce3213a 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1389,11 +1389,7 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): - def __init__( - self, - include_fixed=False, - named_expression_cache=None, - ): + def __init__(self, include_fixed=False, named_expression_cache=None): """Visitor that collects all unique variables participating in an expression @@ -1520,8 +1516,7 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): if named_expression_cache is None: named_expression_cache = {} visitor = _StreamVariableVisitor( - named_expression_cache=named_expression_cache, - include_fixed=include_fixed, + named_expression_cache=named_expression_cache, include_fixed=include_fixed ) variables = visitor.walk_expression(expr) yield from variables From c64dcf459c261ed615e054179cfd614627629dc4 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 21:56:54 -0600 Subject: [PATCH 0696/1178] consolidate logic for adding variable to set --- pyomo/core/expr/visitor.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 1cd2ce3213a..7b519e0f63e 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1417,9 +1417,14 @@ def initializeWalker(self, expr): elif expr.is_named_expression_type(): eid = id(expr) if eid in self._named_expression_cache: + # If we were given a named expression that is already cached, + # just do nothing and return the expression's variables variables, var_set = self._named_expression_cache[eid] return False, variables else: + # We were given a named expression that is not cached. + # Initialize data structures and add this expression to the + # stack. This expression will get popped in exitNode. self._variables = [] self._seen = set() self._named_expression_cache[eid] = [], set() @@ -1442,12 +1447,14 @@ def beforeChild(self, parent, child, index): # the cached variables to our list and don't descend. if self._active_named_expressions: # If we are in another named expression, we update the - # parent expression's cache + # parent expression's cache. We don't need to update the + # global list as we will do this when we exit the active + # named expression. parent_eid = self._active_named_expressions[-1] variables, var_set = self._named_expression_cache[parent_eid] else: # If we are not in a named expression, we update the global - # list + # list. variables = self._variables var_set = self._seen for var in self._named_expression_cache[eid][0]: @@ -1462,16 +1469,16 @@ def beforeChild(self, parent, child, index): self._active_named_expressions.append(id(child)) return True, None elif child.is_variable_type() and (self._include_fixed or not child.fixed): - if id(child) not in self._seen: - self._seen.add(id(child)) - self._variables.append(child) if self._active_named_expressions: # If we are in a named expression, add new variables to the cache. eid = self._active_named_expressions[-1] - local_vars, local_var_set = self._named_expression_cache[eid] - if id(child) not in local_var_set: - local_var_set.add(id(child)) - local_vars.append(child) + variables, var_set = self._named_expression_cache[eid] + else: + variables = self._variables + var_set = self._seen + if id(child) not in local_var_set: + var_set.add(id(child)) + variables.append(child) return False, None else: return True, None From aac6e2f1c49cfb1437b129f76f8edbb017eccda5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 16 Mar 2024 20:12:48 -0400 Subject: [PATCH 0697/1178] Standardize PyROS subordinate solver calls --- pyomo/contrib/pyros/master_problem_methods.py | 96 ++++++------------- .../pyros/separation_problem_methods.py | 45 +++------ pyomo/contrib/pyros/util.py | 78 +++++++++++++++ 3 files changed, 120 insertions(+), 99 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 8b9e85b90e9..2af38c1d582 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -27,6 +27,7 @@ from pyomo.core.expr import value from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.contrib.pyros.util import ( + call_solver, selective_clone, ObjectiveType, pyrosTerminationCondition, @@ -239,31 +240,18 @@ def solve_master_feasibility_problem(model_data, config): else: solver = config.local_solver - timer = TicTocTimer() - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, solver, config - ) - model_data.timing.start_timer("main.master_feasibility") - timer.tic(msg=None) - try: - results = solver.solve(model, tee=config.tee, load_solutions=False) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( + results = call_solver( + model=model, + solver=solver, + config=config, + timing_obj=model_data.timing, + timer_name="main.master_feasibility", + err_msg=( f"Optimizer {repr(solver)} encountered exception " "attempting to solve master feasibility problem in iteration " f"{model_data.iteration}." - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer("main.master_feasibility") - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) feasible_terminations = { tc.optimal, @@ -482,28 +470,18 @@ def minimize_dr_vars(model_data, config): config.progress_logger.debug(f" Initial DR norm: {value(polishing_obj)}") # === Solve the polishing model - timer = TicTocTimer() - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, solver, config - ) - model_data.timing.start_timer("main.dr_polishing") - timer.tic(msg=None) - try: - results = solver.solve(polishing_model, tee=config.tee, load_solutions=False) - except ApplicationError: - config.progress_logger.error( + results = call_solver( + model=polishing_model, + solver=solver, + config=config, + timing_obj=model_data.timing, + timer_name="main.dr_polishing", + err_msg=( f"Optimizer {repr(solver)} encountered an exception " "attempting to solve decision rule polishing problem " f"in iteration {model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer("main.dr_polishing") - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) # interested in the time and termination status for debugging # purposes @@ -726,7 +704,6 @@ def solver_call_master(model_data, config, solver, solve_data): solve_mode = "global" if config.solve_master_globally else "local" config.progress_logger.debug("Solving master problem") - timer = TicTocTimer() for idx, opt in enumerate(solvers): if idx > 0: config.progress_logger.warning( @@ -734,35 +711,18 @@ def solver_call_master(model_data, config, solver, solve_data): f"(solver {idx + 1} of {len(solvers)}) for " f"master problem of iteration {model_data.iteration}." ) - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, opt, config - ) - model_data.timing.start_timer("main.master") - timer.tic(msg=None) - try: - results = opt.solve( - nlp_model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, - ) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( + results = call_solver( + model=nlp_model, + solver=opt, + config=config, + timing_obj=model_data.timing, + timer_name="main.master", + err_msg=( f"Optimizer {repr(opt)} ({idx + 1} of {len(solvers)}) " "encountered exception attempting to " f"solve master problem in iteration {model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer("main.master") - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) optimal_termination = check_optimal_termination(results) infeasible = results.solver.termination_condition == tc.infeasible diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index b5939ff5b19..18d0925bab0 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -18,7 +18,6 @@ from pyomo.core.base import Var, Param from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.dependencies import numpy as np -from pyomo.contrib.pyros.util import ObjectiveType, get_time_from_solver from pyomo.contrib.pyros.solve_data import ( DiscreteSeparationSolveCallResults, SeparationSolveCallResults, @@ -37,9 +36,11 @@ from pyomo.contrib.pyros.util import ABS_CON_CHECK_FEAS_TOL from pyomo.common.timing import TicTocTimer from pyomo.contrib.pyros.util import ( - TIC_TOC_SOLVE_TIME_ATTR, adjust_solver_time_settings, + call_solver, + ObjectiveType, revert_solver_max_time_adjustment, + TIC_TOC_SOLVE_TIME_ATTR, ) import os from copy import deepcopy @@ -1070,6 +1071,7 @@ def solver_call_separation( separation_obj.activate() + solve_mode_adverb = "globally" if solve_globally else "locally" solve_call_results = SeparationSolveCallResults( solved_globally=solve_globally, time_out=False, @@ -1077,7 +1079,6 @@ def solver_call_separation( found_violation=False, subsolver_error=False, ) - timer = TicTocTimer() for idx, opt in enumerate(solvers): if idx > 0: config.progress_logger.warning( @@ -1086,37 +1087,19 @@ def solver_call_separation( f"separation of performance constraint {con_name_repr} " f"in iteration {model_data.iteration}." ) - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, opt, config - ) - model_data.timing.start_timer(f"main.{solve_mode}_separation") - timer.tic(msg=None) - try: - results = opt.solve( - nlp_model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, - ) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - adverb = "globally" if solve_globally else "locally" - config.progress_logger.error( + results = call_solver( + model=nlp_model, + solver=opt, + config=config, + timing_obj=model_data.timing, + timer_name=f"main.{solve_mode}_separation", + err_msg=( f"Optimizer {repr(opt)} ({idx + 1} of {len(solvers)}) " f"encountered exception attempting " - f"to {adverb} solve separation problem for constraint " + f"to {solve_mode_adverb} solve separation problem for constraint " f"{con_name_repr} in iteration {model_data.iteration}." - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer(f"main.{solve_mode}_separation") - finally: - revert_solver_max_time_adjustment( - opt, orig_setting, custom_setting_present, config - ) + ), + ) # record termination condition for this particular solver solver_status_dict[str(opt)] = results.solver.termination_condition diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a3ab3464aa8..33551115148 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -16,7 +16,9 @@ import copy from enum import Enum, auto from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.common.errors import ApplicationError from pyomo.common.modeling import unique_component_name +from pyomo.common.timing import TicTocTimer from pyomo.core.base import ( Constraint, Var, @@ -1731,6 +1733,82 @@ def process_termination_condition_master_problem(config, results): ) +def call_solver(model, solver, config, timing_obj, timer_name, err_msg): + """ + Solve a model with a given optimizer, keeping track of + wall time requirements. + + Parameters + ---------- + model : ConcreteModel + Model of interest. + solver : Pyomo solver type + Subordinate optimizer. + config : ConfigDict + PyROS solver settings. + timing_obj : TimingData + PyROS solver timing data object. + timer_name : str + Name of sub timer under the hierarchical timer contained in + ``timing_obj`` to start/stop for keeping track of solve + time requirements. + err_msg : str + Message to log through ``config.progress_logger.exception()`` + in event an ApplicationError is raised while attempting to + solve the model. + + Returns + ------- + SolverResults + Solve results. Note that ``results.solver`` contains + an additional attribute, named after + ``TIC_TOC_SOLVE_TIME_ATTR``, of which the value is set to the + recorded solver wall time. + + Raises + ------ + ApplicationError + If ApplicationError is raised by the solver. + In this case, `err_msg` is logged through + ``config.progress_logger.exception()`` before + the excception is raised. + """ + tt_timer = TicTocTimer() + + orig_setting, custom_setting_present = adjust_solver_time_settings( + timing_obj, solver, config + ) + timing_obj.start_timer(timer_name) + tt_timer.tic(msg=None) + + try: + results = solver.solve( + model, + tee=config.tee, + load_solutions=False, + symbolic_solver_labels=True, + ) + except ApplicationError: + # account for possible external subsolver errors + # (such as segmentation faults, function evaluation + # errors, etc.) + config.progress_logger.error(err_msg) + raise + else: + setattr( + results.solver, + TIC_TOC_SOLVE_TIME_ATTR, + tt_timer.toc(msg=None, delta=True), + ) + finally: + timing_obj.stop_timer(timer_name) + revert_solver_max_time_adjustment( + solver, orig_setting, custom_setting_present, config + ) + + return results + + class IterationLogRecord: """ PyROS solver iteration log record. From d9f22516d0b79d204462ffb91095b408423de524 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 15:57:18 -0400 Subject: [PATCH 0698/1178] Account for user settings in subsolver time limit adjustment --- pyomo/contrib/pyros/util.py | 68 +++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 33551115148..7d40d357863 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -232,15 +232,15 @@ def get_main_elapsed_time(timing_data_obj): def adjust_solver_time_settings(timing_data_obj, solver, config): """ - Adjust solver max time setting based on current PyROS elapsed - time. + Adjust maximum time allowed for subordinate solver, based + on total PyROS solver elapsed time up to this point. Parameters ---------- timing_data_obj : Bunch PyROS timekeeper. solver : solver type - Solver for which to adjust the max time setting. + Subordinate solver for which to adjust the max time setting. config : ConfigDict PyROS solver config. @@ -262,26 +262,40 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): ---- (1) Adjustment only supported for GAMS, BARON, and IPOPT interfaces. This routine can be generalized to other solvers - after a generic interface to the time limit setting + after a generic Pyomo interface to the time limit setting is introduced. - (2) For IPOPT, and probably also BARON, the CPU time limit - rather than the wallclock time limit, is adjusted, as - no interface to wallclock limit available. - For this reason, extra 30s is added to time remaining - for subsolver time limit. - (The extra 30s is large enough to ensure solver - elapsed time is not beneath elapsed time - user time limit, - but not so large as to overshoot the user-specified time limit - by an inordinate margin.) + (2) For IPOPT and BARON, the CPU time limit, + rather than the wallclock time limit, may be adjusted, + as there may be no means by which to specify the wall time + limit explicitly. + (3) For GAMS, we adjust the time limit through the GAMS Reslim + option. However, this may be overriden by any user + specifications included in a GAMS optfile, which may be + difficult to track down. + (3) To ensure the time limit is specified to a strictly + positive value, the time limit is adjusted to a value of + at least 1 second. """ + # in case there is no time remaining: we set time limit + # to a minimum of 1s, as some solvers require a strictly + # positive time limit + time_limit_buffer = 1 + if config.time_limit is not None: time_remaining = config.time_limit - get_main_elapsed_time(timing_data_obj) if isinstance(solver, type(SolverFactory("gams", solver_io="shell"))): original_max_time_setting = solver.options["add_options"] custom_setting_present = "add_options" in solver.options - # adjust GAMS solver time - reslim_str = f"option reslim={max(30, 30 + time_remaining)};" + # note: our time limit will be overriden by any + # time limits specified by the user through a + # GAMS optfile, but tracking down the optfile + # and/or the GAMS subsolver specific option + # is more difficult + reslim_str = ( + "option reslim=" + f"{max(time_limit_buffer, time_remaining)};" + ) if isinstance(solver.options["add_options"], list): solver.options["add_options"].append(reslim_str) else: @@ -291,7 +305,13 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): if isinstance(solver, SolverFactory.get_class("baron")): options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): - options_key = "max_cpu_time" + options_key = ( + # IPOPT 3.14.0+ added support for specifying + # wall time limit explicitly; this is preferred + # over CPU time limit + "max_wall_time" if solver.version() >= (3, 14, 0, 0) + else "max_cpu_time" + ) else: options_key = None @@ -299,8 +319,20 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): custom_setting_present = options_key in solver.options original_max_time_setting = solver.options[options_key] - # ensure positive value assigned to avoid application error - solver.options[options_key] = max(30, 30 + time_remaining) + # account for elapsed time remaining and + # original time limit setting. + # if no original time limit is set, then we assume + # there is no time limit, rather than tracking + # down the solver-specific default + orig_max_time = ( + float("inf") + if original_max_time_setting is None + else original_max_time_setting + ) + solver.options[options_key] = min( + max(time_limit_buffer, time_remaining), + orig_max_time, + ) else: custom_setting_present = False original_max_time_setting = None From 2593fce468e4f8095a2c2c35698323155035d2e8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:05:38 -0400 Subject: [PATCH 0699/1178] Fix test error message string --- pyomo/contrib/pyros/tests/test_grcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c308f0d6990..754ab6678ea 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4398,8 +4398,8 @@ def test_gams_successful_time_limit(self): results.pyros_termination_condition, pyrosTerminationCondition.robust_optimal, msg=( - f"Returned termination condition with local " - "subsolver {idx + 1} of 2 is not robust_optimal." + "Returned termination condition with local " + f"subsolver {idx + 1} of 2 is not robust_optimal." ), ) From 679dc7ee4620a1d424d055250363e84d24bca86c Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:38:48 -0400 Subject: [PATCH 0700/1178] Add support for SCIP time limit adjustment --- pyomo/contrib/pyros/tests/test_grcs.py | 77 +++++++------------------- pyomo/contrib/pyros/util.py | 4 ++ 2 files changed, 24 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 754ab6678ea..92532677a80 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4345,10 +4345,10 @@ def test_separation_terminate_time_limit(self): and SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.", ) - def test_gams_successful_time_limit(self): + def test_pyros_subsolver_time_limit_adjustment(self): """ - Test PyROS time limit status returned in event - separation problem times out. + Check that PyROS does not ultimately alter state of + subordinate solver options due to time limit adjustments. """ m = ConcreteModel() m.x1 = Var(initialize=0, bounds=(0, None)) @@ -4367,20 +4367,26 @@ def test_gams_successful_time_limit(self): # Instantiate the PyROS solver pyros_solver = SolverFactory("pyros") - # Define subsolvers utilized in the algorithm - # two GAMS solvers, one of which has reslim set - # (overridden when invoked in PyROS) + # subordinate solvers to test. + # for testing, we pass each as the 'local' solver, + # and the BARON solver without custom options + # as the 'global' solver + baron_no_options = SolverFactory("baron") local_subsolvers = [ SolverFactory("gams:conopt"), SolverFactory("gams:conopt"), SolverFactory("ipopt"), + SolverFactory("ipopt", options={"max_cpu_time": 300}), + SolverFactory("scip"), + SolverFactory("scip", options={"limits/time": 300}), + baron_no_options, + SolverFactory("baron", options={"MaxTime": 300}), ] local_subsolvers[0].options["add_options"] = ["option reslim=100;"] - global_subsolver = SolverFactory("baron") - global_subsolver.options["MaxTime"] = 300 # Call the PyROS solver for idx, opt in enumerate(local_subsolvers): + original_solver_options = opt.options.copy() results = pyros_solver.solve( model=m, first_stage_variables=[m.x1, m.x2], @@ -4388,12 +4394,11 @@ def test_gams_successful_time_limit(self): uncertain_params=[m.u], uncertainty_set=interval, local_solver=opt, - global_solver=global_subsolver, + global_solver=baron_no_options, objective_focus=ObjectiveType.worst_case, solve_master_globally=True, time_limit=100, ) - self.assertEqual( results.pyros_termination_condition, pyrosTerminationCondition.robust_optimal, @@ -4402,54 +4407,12 @@ def test_gams_successful_time_limit(self): f"subsolver {idx + 1} of 2 is not robust_optimal." ), ) - - # check first local subsolver settings - # remain unchanged after PyROS exit - self.assertEqual( - len(list(local_subsolvers[0].options["add_options"])), - 1, - msg=( - f"Local subsolver {local_subsolvers[0]} options 'add_options'" - "were changed by PyROS" - ), - ) - self.assertEqual( - local_subsolvers[0].options["add_options"][0], - "option reslim=100;", - msg=( - f"Local subsolver {local_subsolvers[0]} setting " - "'add_options' was modified " - "by PyROS, but changes were not properly undone" - ), - ) - - # check global subsolver settings unchanged - self.assertEqual( - len(list(global_subsolver.options.keys())), - 1, - msg=(f"Global subsolver {global_subsolver} options were changed by PyROS"), - ) - self.assertEqual( - global_subsolver.options["MaxTime"], - 300, - msg=( - f"Global subsolver {global_subsolver} setting " - "'MaxTime' was modified " - "by PyROS, but changes were not properly undone" - ), - ) - - # check other local subsolvers remain unchanged - for slvr, key in zip(local_subsolvers[1:], ["add_options", "max_cpu_time"]): - # no custom options were added to the `options` - # attribute of the optimizer, so any attribute - # of `options` should be `None` - self.assertIs( - getattr(slvr.options, key, None), - None, + self.assertEqual( + opt.options, + original_solver_options, msg=( - f"Local subsolver {slvr} setting '{key}' was added " - "by PyROS, but not reverted" + f"Options for subordinate solver {opt} were changed " + "by PyROS, and the changes wee not properly reverted." ), ) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 7d40d357863..bdec2213d43 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -312,6 +312,8 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): "max_wall_time" if solver.version() >= (3, 14, 0, 0) else "max_cpu_time" ) + elif isinstance(solver, SolverFactory.get_class("scip")): + options_key = "limits/time" else: options_key = None @@ -379,6 +381,8 @@ def revert_solver_max_time_adjustment( options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): options_key = "max_cpu_time" + elif isinstance(solver, SolverFactory.get_class("scip")): + options_key = "limits/time" else: options_key = None From fcb28193147e55e18f69128d10060d2b8839ca8b Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:42:21 -0400 Subject: [PATCH 0701/1178] Simplify time limit adjustment reversion --- pyomo/contrib/pyros/util.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index bdec2213d43..fa423e37450 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -397,12 +397,7 @@ def revert_solver_max_time_adjustment( if isinstance(solver, type(SolverFactory("gams", solver_io="shell"))): solver.options[options_key].pop() else: - # remove the max time specification introduced. - # All lines are needed here to completely remove the option - # from access through getattr and dictionary reference. delattr(solver.options, options_key) - if options_key in solver.options.keys(): - del solver.options[options_key] class PreformattedLogger(logging.Logger): From 0c8afa56489f3c32d987b75ab65038538d3e9735 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:56:22 -0400 Subject: [PATCH 0702/1178] Update solver test availability and license check --- pyomo/contrib/pyros/tests/test_grcs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 92532677a80..41223b30899 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4341,9 +4341,11 @@ def test_separation_terminate_time_limit(self): ) @unittest.skipUnless( - SolverFactory('gams').license_is_valid() - and SolverFactory('baron').license_is_valid(), - "Global NLP solver is not available and licensed.", + ipopt_available + and SolverFactory('gams').license_is_valid() + and SolverFactory('baron').license_is_valid() + and SolverFactory("scip").license_is_valid(), + "IPOPT not available or one of GAMS/BARON/SCIP not licensed", ) def test_pyros_subsolver_time_limit_adjustment(self): """ From ec830e6c19600419a3a187c7c63c9f1700bedfcf Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 19:05:33 -0400 Subject: [PATCH 0703/1178] Move PyROS timer start to before argument validation --- pyomo/contrib/pyros/pyros.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 6de42d7299e..c74daf34c5f 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -330,32 +330,24 @@ def solve( Summary of PyROS termination outcome. """ - kwds.update( - dict( - first_stage_variables=first_stage_variables, - second_stage_variables=second_stage_variables, - uncertain_params=uncertain_params, - uncertainty_set=uncertainty_set, - local_solver=local_solver, - global_solver=global_solver, - ) - ) - config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) - - # === Create data containers model_data = ROSolveResults() - model_data.timing = Bunch() - - # === Start timer, run the algorithm model_data.timing = TimingData() with time_code( timing_data_obj=model_data.timing, code_block_name="main", is_main_timer=True, ): - # output intro and disclaimer - self._log_intro(logger=config.progress_logger, level=logging.INFO) - self._log_disclaimer(logger=config.progress_logger, level=logging.INFO) + kwds.update( + dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + ) + ) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) self._log_config( logger=config.progress_logger, config=config, From 348a896bb77f2ad2634043647303e325edd1e06f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 19:14:24 -0400 Subject: [PATCH 0704/1178] Fix typos --- pyomo/contrib/pyros/util.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index fa423e37450..306141e9829 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -269,7 +269,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): as there may be no means by which to specify the wall time limit explicitly. (3) For GAMS, we adjust the time limit through the GAMS Reslim - option. However, this may be overriden by any user + option. However, this may be overridden by any user specifications included in a GAMS optfile, which may be difficult to track down. (3) To ensure the time limit is specified to a strictly @@ -287,15 +287,12 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): original_max_time_setting = solver.options["add_options"] custom_setting_present = "add_options" in solver.options - # note: our time limit will be overriden by any + # note: our time limit will be overridden by any # time limits specified by the user through a # GAMS optfile, but tracking down the optfile # and/or the GAMS subsolver specific option # is more difficult - reslim_str = ( - "option reslim=" - f"{max(time_limit_buffer, time_remaining)};" - ) + reslim_str = "option reslim=" f"{max(time_limit_buffer, time_remaining)};" if isinstance(solver.options["add_options"], list): solver.options["add_options"].append(reslim_str) else: @@ -309,7 +306,8 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): # IPOPT 3.14.0+ added support for specifying # wall time limit explicitly; this is preferred # over CPU time limit - "max_wall_time" if solver.version() >= (3, 14, 0, 0) + "max_wall_time" + if solver.version() >= (3, 14, 0, 0) else "max_cpu_time" ) elif isinstance(solver, SolverFactory.get_class("scip")): @@ -332,8 +330,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): else original_max_time_setting ) solver.options[options_key] = min( - max(time_limit_buffer, time_remaining), - orig_max_time, + max(time_limit_buffer, time_remaining), orig_max_time ) else: custom_setting_present = False @@ -1814,10 +1811,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): try: results = solver.solve( - model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, + model, tee=config.tee, load_solutions=False, symbolic_solver_labels=True ) except ApplicationError: # account for possible external subsolver errors @@ -1827,9 +1821,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): raise else: setattr( - results.solver, - TIC_TOC_SOLVE_TIME_ATTR, - tt_timer.toc(msg=None, delta=True), + results.solver, TIC_TOC_SOLVE_TIME_ATTR, tt_timer.toc(msg=None, delta=True) ) finally: timing_obj.stop_timer(timer_name) From fed3c33dc2626e78d51e6025af9d93751b5e4313 Mon Sep 17 00:00:00 2001 From: robbybp Date: Sun, 17 Mar 2024 20:38:50 -0600 Subject: [PATCH 0705/1178] fix typo --- pyomo/core/expr/visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 7b519e0f63e..2fddca22c5f 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1476,7 +1476,7 @@ def beforeChild(self, parent, child, index): else: variables = self._variables var_set = self._seen - if id(child) not in local_var_set: + if id(child) not in var_set: var_set.add(id(child)) variables.append(child) return False, None From 27ac97ff96f37c4f43c10501c0448d3f42532c67 Mon Sep 17 00:00:00 2001 From: Utkarsh-Detha Date: Mon, 18 Mar 2024 12:25:46 +0100 Subject: [PATCH 0706/1178] Fix: mosek_direct updated to use putqconk instead of putqcon This fix concerns QCQP models when solved using mosek. In MOSEK's Optimizer API, the putqcon method resets the Q matrix entries for all constraints to zero, while putqconk does so only for k-th constraint. The _add_constraints method in mosek_direct would call putqcon, but this would lead to loss of Q info with every subsequent call to the _add_constraints (if new Q info was given). This is now fixed, because the Q matrix in each constraint is updated in its own call to putqconk. --- pyomo/solvers/plugins/solvers/mosek_direct.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/mosek_direct.py b/pyomo/solvers/plugins/solvers/mosek_direct.py index 5000a2f35c4..f4225651907 100644 --- a/pyomo/solvers/plugins/solvers/mosek_direct.py +++ b/pyomo/solvers/plugins/solvers/mosek_direct.py @@ -492,13 +492,10 @@ def _add_constraints(self, con_seq): ptrb = (0,) + ptre[:-1] asubs = tuple(itertools.chain.from_iterable(l_ids)) avals = tuple(itertools.chain.from_iterable(l_coefs)) - qcsubi = tuple(itertools.chain.from_iterable(q_is)) - qcsubj = tuple(itertools.chain.from_iterable(q_js)) - qcval = tuple(itertools.chain.from_iterable(q_vals)) - qcsubk = tuple(i for i in sub for j in range(len(q_is[i - con_num]))) self._solver_model.appendcons(num_lq) self._solver_model.putarowlist(sub, ptrb, ptre, asubs, avals) - self._solver_model.putqcon(qcsubk, qcsubi, qcsubj, qcval) + for k, i, j, v in zip(sub, q_is, q_js, q_vals): + self._solver_model.putqconk(k, i, j, v) self._solver_model.putconboundlist(sub, bound_types, lbs, ubs) for i, s_n in enumerate(sub_names): self._solver_model.putconname(sub[i], s_n) From dbe0529350c26de25f9acf71657e595ae22d90d9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 18 Mar 2024 12:57:50 -0400 Subject: [PATCH 0707/1178] Update version number, changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 11 +++++++++++ pyomo/contrib/pyros/pyros.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 94f4848edb2..52cd7a6db47 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,17 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.11 17 Mar 2024 +------------------------------------------------------------------------------- +- Standardize calls to subordinate solvers across all PyROS subproblem types +- Account for user-specified subsolver time limits when automatically + adjusting subsolver time limits +- Add support for automatic adjustment of SCIP subsolver time limit +- Move start point of main PyROS solver timer to just before argument + validation begins + + ------------------------------------------------------------------------------- PyROS 1.2.10 07 Feb 2024 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index c74daf34c5f..c3335588b7b 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -44,7 +44,7 @@ from datetime import datetime -__version__ = "1.2.10" +__version__ = "1.2.11" default_pyros_solver_logger = setup_pyros_logger() From 927c46c660189526e4728c8c0b37fcf82ae94bce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:25:11 -0600 Subject: [PATCH 0708/1178] Add 'mixed' option to standard form writer --- pyomo/repn/plugins/standard_form.py | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 239cd845930..d0e1014d549 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -139,6 +139,15 @@ class LinearStandardFormCompiler(object): description='Add slack variables and return `min cTx s.t. Ax == b`', ), ) + CONFIG.declare( + 'mixed_form', + ConfigValue( + default=False, + domain=bool, + description='Return A in mixed form (the comparison operator is a ' + 'mix of <=, ==, and >=)', + ), + ) CONFIG.declare( 'show_section_timing', ConfigValue( @@ -332,6 +341,9 @@ def write(self, model): # Tabulate constraints # slack_form = self.config.slack_form + mixed_form = self.config.mixed_form + if slack_form and mixed_form: + raise ValueError("cannot specify both slack_form and mixed_form") rows = [] rhs = [] con_data = [] @@ -372,7 +384,30 @@ def write(self, model): f"model contains a trivially infeasible constraint, '{con.name}'" ) - if slack_form: + if mixed_form: + N = len(repn.linear) + _data = np.fromiter(repn.linear.values(), float, N) + _index = np.fromiter(map(var_order.__getitem__, repn.linear), float, N) + if ub == lb: + rows.append(RowEntry(con, 0)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + else: + if ub is not None: + rows.append(RowEntry(con, 1)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + if lb is not None: + rows.append(RowEntry(con, -1)) + rhs.append(lb - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + elif slack_form: _data = list(repn.linear.values()) _index = list(map(var_order.__getitem__, repn.linear)) if lb == ub: # TODO: add tolerance? From 64211e187f5a3daa2d0d1c4c4061aae4866c4b44 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:25:37 -0600 Subject: [PATCH 0709/1178] Fix error when removing unused variables --- pyomo/repn/plugins/standard_form.py | 26 ++++++++++++-------------- pyomo/repn/tests/test_standard_form.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index d0e1014d549..ea7b6a6a9e6 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -472,24 +472,22 @@ def write(self, model): # at the index pointer list (an O(num_var) operation). c_ip = c.indptr A_ip = A.indptr - active_var_idx = list( - filter( - lambda i: A_ip[i] != A_ip[i + 1] or c_ip[i] != c_ip[i + 1], - range(len(columns)), - ) - ) - nCol = len(active_var_idx) + active_var_mask = (A_ip[1:] > A_ip[:-1]) | (c_ip[1:] > c_ip[:-1]) + + # Masks on NumPy arrays are very fast. Build the reduced A + # indptr and then check if we actually have to manipulate the + # columns + augmented_mask = np.concatenate((active_var_mask, [True])) + reduced_A_indptr = A.indptr[augmented_mask] + nCol = len(reduced_A_indptr) - 1 if nCol != len(columns): - # Note that the indptr can't just use range() because a var - # may only appear in the objectives or the constraints. - columns = list(map(columns.__getitem__, active_var_idx)) - active_var_idx.append(c.indptr[-1]) + columns = [v for k, v in zip(active_var_mask, columns) if k] c = scipy.sparse.csc_array( - (c.data, c.indices, c.indptr.take(active_var_idx)), [c.shape[0], nCol] + (c.data, c.indices, c.indptr[augmented_mask]), [c.shape[0], nCol] ) - active_var_idx[-1] = A.indptr[-1] + # active_var_idx[-1] = len(columns) A = scipy.sparse.csc_array( - (A.data, A.indices, A.indptr.take(active_var_idx)), [A.shape[0], nCol] + (A.data, A.indices, reduced_A_indptr), [A.shape[0], nCol] ) if self.config.nonnegative_vars: diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index e24195edfde..c8b914deca5 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -43,6 +43,19 @@ def test_linear_model(self): self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + def test_almost_dense_linear_model(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] + 4 * m.y[3] >= 10) + m.d = pyo.Constraint(expr=5 * m.x + 6 * m.y[1] + 8 * m.y[3] <= 20) + + repn = LinearStandardFormCompiler().write(m) + + self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) + self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) + self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + def test_linear_model_row_col_order(self): m = pyo.ConcreteModel() m.x = pyo.Var() From 4110f005d2f3a7b1bc97e9b5db997853da42c238 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:34:00 -0600 Subject: [PATCH 0710/1178] Make LegacySolverWrapper compatible with Pyomo script --- pyomo/contrib/solver/base.py | 110 ++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 43d168a98a0..29b2569278c 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -19,9 +19,10 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.config import document_kwargs_from_configdict +from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning +from pyomo.common.modeling import NOTSET from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -347,6 +348,11 @@ class LegacySolverWrapper: interface. Necessary for backwards compatibility. """ + def __init__(self, solver_io=None, **kwargs): + if solver_io is not None: + raise NotImplementedError('Still working on this') + super().__init__(**kwargs) + # # Support "with" statements # @@ -358,51 +364,57 @@ def __exit__(self, t, v, traceback): def _map_config( self, - tee, - load_solutions, - symbolic_solver_labels, - timelimit, - # Report timing is no longer a valid option. We now always return a - # timer object that can be inspected. - report_timing, - raise_exception_on_nonoptimal_result, - solver_io, - suffixes, - logfile, - keepfiles, - solnfile, - options, + tee=NOTSET, + load_solutions=NOTSET, + symbolic_solver_labels=NOTSET, + timelimit=NOTSET, + report_timing=NOTSET, + raise_exception_on_nonoptimal_result=NOTSET, + solver_io=NOTSET, + suffixes=NOTSET, + logfile=NOTSET, + keepfiles=NOTSET, + solnfile=NOTSET, + options=NOTSET, ): """Map between legacy and new interface configuration options""" self.config = self.config() - self.config.tee = tee - self.config.load_solutions = load_solutions - self.config.symbolic_solver_labels = symbolic_solver_labels - self.config.time_limit = timelimit - self.config.solver_options.set_value(options) + if tee is not NOTSET: + self.config.tee = tee + if load_solutions is not NOTSET: + self.config.load_solutions = load_solutions + if symbolic_solver_labels is not NOTSET: + self.config.symbolic_solver_labels = symbolic_solver_labels + if timelimit is not NOTSET: + self.config.time_limit = timelimit + if report_timing is not NOTSET: + self.config.report_timing = report_timing + if options is not NOTSET: + self.config.solver_options.set_value(options) # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" - self.config.raise_exception_on_nonoptimal_result = ( - raise_exception_on_nonoptimal_result - ) - if solver_io is not None: + if raise_exception_on_nonoptimal_result is not NOTSET: + self.config.raise_exception_on_nonoptimal_result = ( + raise_exception_on_nonoptimal_result + ) + if solver_io is not NOTSET: raise NotImplementedError('Still working on this') - if suffixes is not None: + if suffixes is not NOTSET: raise NotImplementedError('Still working on this') - if logfile is not None: + if logfile is not NOTSET: raise NotImplementedError('Still working on this') if keepfiles or 'keepfiles' in self.config: cwd = os.getcwd() deprecation_warning( "`keepfiles` has been deprecated in the new solver interface. " - "Use `working_dir` instead to designate a directory in which " - f"files should be generated and saved. Setting `working_dir` to `{cwd}`.", + "Use `working_dir` instead to designate a directory in which files " + f"should be generated and saved. Setting `working_dir` to `{cwd}`.", version='6.7.1', ) self.config.working_dir = cwd # I believe this currently does nothing; however, it is unclear what # our desired behavior is for this. - if solnfile is not None: + if solnfile is not NOTSET: if 'filename' in self.config: filename = os.path.splitext(solnfile)[0] self.config.filename = filename @@ -504,20 +516,24 @@ def solve( """ original_config = self.config - self._map_config( - tee, - load_solutions, - symbolic_solver_labels, - timelimit, - report_timing, - raise_exception_on_nonoptimal_result, - solver_io, - suffixes, - logfile, - keepfiles, - solnfile, - options, + + map_args = ( + 'tee', + 'load_solutions', + 'symbolic_solver_labels', + 'timelimit', + 'report_timing', + 'raise_exception_on_nonoptimal_result', + 'solver_io', + 'suffixes', + 'logfile', + 'keepfiles', + 'solnfile', + 'options', ) + loc = locals() + filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None} + self._map_config(**filtered_args) results: Results = super().solve(model) legacy_results, legacy_soln = self._map_results(model, results) @@ -555,3 +571,13 @@ def license_is_valid(self) -> bool: """ return bool(self.available()) + + def config_block(self, init=False): + from pyomo.scripting.solve_config import default_config_block + + return default_config_block(self, init)[0] + + def set_options(self, options): + opts = {k: v for k, v in options.value().items() if v is not None} + if opts: + self._map_config(**opts) From 62b861a9811021f9c54a6aed4fe73ca841164136 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:34:33 -0600 Subject: [PATCH 0711/1178] Make report_timing 'work' in LegactSolverInterface --- pyomo/contrib/solver/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 29b2569278c..9e0356c9c21 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -379,6 +379,10 @@ def _map_config( ): """Map between legacy and new interface configuration options""" self.config = self.config() + if 'report_timing' not in self.config: + self.config.declare( + 'report_timing', ConfigValue(domain=bool, default=False) + ) if tee is not NOTSET: self.config.tee = tee if load_solutions is not NOTSET: @@ -537,11 +541,13 @@ def solve( results: Results = super().solve(model) legacy_results, legacy_soln = self._map_results(model, results) - legacy_results = self._solution_handler( load_solutions, model, results, legacy_results, legacy_soln ) + if self.config.report_timing: + print(results.timing_info.timer) + self.config = original_config return legacy_results From c097f03d92a248835365823db52945ef05f3fe93 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:37:06 -0600 Subject: [PATCH 0712/1178] Initial draft of a new numpy-based Gurobi Direct interface --- pyomo/contrib/solver/gurobi_direct.py | 349 ++++++++++++++++++++++++++ pyomo/contrib/solver/plugins.py | 6 + 2 files changed, 355 insertions(+) create mode 100644 pyomo/contrib/solver/gurobi_direct.py diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py new file mode 100644 index 00000000000..56047b6c2c7 --- /dev/null +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -0,0 +1,349 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import datetime +import io +import math + +from pyomo.common.config import ConfigValue +from pyomo.common.dependencies import attempt_import +from pyomo.common.shutdown import python_is_shutting_down +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer + +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition +from pyomo.contrib.solver.solution import SolutionLoaderBase + +from pyomo.core.staleflag import StaleFlagManager + +from pyomo.repn.plugins.standard_form import LinearStandardFormCompiler + +gurobipy, gurobipy_available = attempt_import('gurobipy') + + +class GurobiConfig(BranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(GurobiConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_mipstart: bool = self.declare( + 'use_mipstart', + ConfigValue( + default=False, + domain=bool, + description="If True, the values of the integer variables will be passed to Gurobi.", + ), + ) + + +class GurobiDirectSolutionLoader(SolutionLoaderBase): + def __init__(self, grb_model, grb_vars, pyo_vars): + self._grb_model = grb_model + self._grb_vars = grb_vars + self._pyo_vars = pyo_vars + GurobiDirect._num_instances += 1 + + def __del__(self): + if not python_is_shutting_down(): + GurobiDirect._num_instances -= 1 + if GurobiDirect._num_instances == 0: + GurobiDirect.release_license() + + def load_vars(self, vars_to_load=None, solution_number=0): + assert vars_to_load is None + assert solution_number == 0 + for p_var, g_var in zip(self._pyo_vars, self._grb_vars.x.tolist()): + p_var.set_value(g_var, skip_validation=True) + + def get_primals(self, vars_to_load=None): + assert vars_to_load is None + assert solution_number == 0 + return ComponentMap(zip(self._pyo_vars, self._grb_vars.x.tolist())) + + +class GurobiDirect(SolverBase): + CONFIG = GurobiConfig() + + _available = None + _num_instances = 0 + + def __init__(self, **kwds): + super().__init__(**kwds) + GurobiDirect._num_instances += 1 + + def available(self): + if not gurobipy_available: # this triggers the deferred import + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + return self._check_license() + + def _check_license(self): + avail = False + try: + # Gurobipy writes out license file information when creating + # the environment + with capture_output(capture_fd=True): + m = gurobipy.Model() + avail = True + except gurobipy.GurobiError: + avail = False + + if avail: + if self._available is None: + self._available = GurobiDirect._check_full_license(m) + return self._available + else: + return self.Availability.BadLicense + + @classmethod + def _check_full_license(cls, model=None): + if model is None: + model = gurobipy.Model() + model.setParam('OutputFlag', 0) + try: + model.addVars(range(2001)) + model.optimize() + return cls.Availability.FullLicense + except gurobipy.GurobiError: + return cls.Availability.LimitedLicense + + def __del__(self): + if not python_is_shutting_down(): + GurobiDirect._num_instances -= 1 + if GurobiDirect._num_instances == 0: + self.release_license() + + @staticmethod + def release_license(): + if gurobipy_available: + with capture_output(capture_fd=True): + gurobipy.disposeDefaultEnv() + + def version(self): + version = ( + gurobipy.GRB.VERSION_MAJOR, + gurobipy.GRB.VERSION_MINOR, + gurobipy.GRB.VERSION_TECHNICAL, + ) + return version + + def solve(self, model, **kwds) -> Results: + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + self._config = config = self.config(value=kwds, preserve_implicit=True) + StaleFlagManager.mark_all_as_stale() + if config.timer is None: + config.timer = HierarchicalTimer() + timer = config.timer + + timer.start('compile_model') + repn = LinearStandardFormCompiler().write(model, mixed_form=True) + timer.stop('compile_model') + + timer.start('prepare_matrices') + inf = float('inf') + ninf = -inf + lb = [] + ub = [] + for v in repn.columns: + _l, _u = v.bounds + if _l is None: + _l = ninf + if _u is None: + _u = inf + lb.append(_l) + ub.append(_u) + vtype = [ + ( + gurobipy.GRB.CONTINUOUS + if v.is_continuous() + else ( + gurobipy.GRB.BINARY + if v.is_binary() + else gurobipy.GRB.INTEGER if v.is_integer() else '?' + ) + ) + for v in repn.columns + ] + sense_type = '>=<' + sense = [sense_type[r[1] + 1] for r in repn.rows] + timer.stop('prepare_matrices') + + ostreams = [io.StringIO()] + config.tee + + try: + orig_cwd = os.getcwd() + if self._config.working_directory: + os.chdir(self._config.working_directory) + with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): + gurobi_model = gurobipy.Model() + + timer.start('transfer_model') + x = gurobi_model.addMVar( + len(repn.columns), + lb=lb, + ub=ub, + obj=repn.c.todense()[0], + vtype=vtype, + ) + A = gurobi_model.addMConstr(repn.A, x, sense, repn.rhs) + # gurobi_model.update() + timer.stop('transfer_model') + + options = config.solver_options + + gurobi_model.setParam('LogToConsole', 1) + + if config.threads is not None: + gurobi_model.setParam('Threads', config.threads) + if config.time_limit is not None: + gurobi_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + gurobi_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + gurobi_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + raise MouseTrap("MIPSTART not yet supported") + + for key, option in options.items(): + gurobi_model.setParam(key, option) + + timer.start('optimize') + gurobi_model.optimize() + timer.stop('optimize') + finally: + os.chdir(orig_cwd) + + res = self._postsolve( + timer, GurobiDirectSolutionLoader(gurobi_model, x, repn.columns) + ) + res.solver_configuration = config + res.solver_name = 'Gurobi' + res.solver_version = self.version() + res.solver_log = ostreams[0].getvalue() + + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + res.timing_info.start_timestamp = start_timestamp + res.timing_info.wall_time = (end_timestamp - start_timestamp).total_seconds() + res.timing_info.timer = timer + return res + + def _postsolve(self, timer: HierarchicalTimer, loader): + config = self._config + + gprob = loader._grb_model + grb = gurobipy.GRB + status = gprob.Status + + results = Results() + results.solution_loader = loader + results.timing_info.gurobi_time = gprob.Runtime + + if gprob.SolCount > 0: + if status == grb.OPTIMAL: + results.solution_status = SolutionStatus.optimal + else: + results.solution_status = SolutionStatus.feasible + else: + results.solution_status = SolutionStatus.noSolution + + if status == grb.LOADED: # problem is loaded, but no solution + results.termination_condition = TerminationCondition.unknown + elif status == grb.OPTIMAL: # optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + elif status == grb.INFEASIBLE: + results.termination_condition = TerminationCondition.provenInfeasible + elif status == grb.INF_OR_UNBD: + results.termination_condition = TerminationCondition.infeasibleOrUnbounded + elif status == grb.UNBOUNDED: + results.termination_condition = TerminationCondition.unbounded + elif status == grb.CUTOFF: + results.termination_condition = TerminationCondition.objectiveLimit + elif status == grb.ITERATION_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.NODE_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.TIME_LIMIT: + results.termination_condition = TerminationCondition.maxTimeLimit + elif status == grb.SOLUTION_LIMIT: + results.termination_condition = TerminationCondition.unknown + elif status == grb.INTERRUPTED: + results.termination_condition = TerminationCondition.interrupted + elif status == grb.NUMERIC: + results.termination_condition = TerminationCondition.unknown + elif status == grb.SUBOPTIMAL: + results.termination_condition = TerminationCondition.unknown + elif status == grb.USER_OBJ_LIMIT: + results.termination_condition = TerminationCondition.objectiveLimit + else: + results.termination_condition = TerminationCondition.unknown + + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + and config.raise_exception_on_nonoptimal_result + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) + + results.incumbent_objective = None + results.objective_bound = None + try: + results.incumbent_objective = gprob.ObjVal + except (gurobipy.GurobiError, AttributeError): + results.incumbent_objective = None + try: + results.objective_bound = gprob.ObjBound + except (gurobipy.GurobiError, AttributeError): + if self._objective.sense == minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective + ): + results.incumbent_objective = None + + results.iteration_count = gprob.getAttr('IterCount') + + timer.start('load solution') + if config.load_solutions: + if gprob.SolCount > 0: + results.solution_loader.load_vars() + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False and check ' + 'results.solution_status and ' + 'results.incumbent_objective before loading a solution.' + ) + timer.stop('load solution') + + return results diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index c7da41463a2..b0beef185de 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -13,6 +13,7 @@ from .factory import SolverFactory from .ipopt import Ipopt from .gurobi import Gurobi +from .gurobi_direct import GurobiDirect def load(): @@ -22,3 +23,8 @@ def load(): SolverFactory.register( name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' )(Gurobi) + SolverFactory.register( + name='gurobi_direct', + legacy_name='gurobi_direct_v2', + doc='Direct (scipy-based) interface to Gurobi', + )(GurobiDirect) From 3f2b62a2d9f31881f8130882d0d7fe22daa495ad Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:35:12 -0600 Subject: [PATCH 0713/1178] Clean up automatic LegacySolverFactory registrations --- pyomo/contrib/solver/factory.py | 4 +++- pyomo/contrib/solver/ipopt.py | 2 -- pyomo/contrib/solver/plugins.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 91ce92a9dee..8861534bd01 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -27,7 +27,9 @@ def decorator(cls): class LegacySolver(LegacySolverWrapper, cls): pass - LegacySolverFactory.register(legacy_name, doc)(LegacySolver) + LegacySolverFactory.register(legacy_name + " (new interface)", doc)( + LegacySolver + ) return cls diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index edc5799ae20..5f601b7a9f7 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -30,7 +30,6 @@ from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.contrib.solver.sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolSolutionLoader @@ -197,7 +196,6 @@ def get_reduced_costs( } -@SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)') class Ipopt(SolverBase): CONFIG = IpoptConfig() diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index c7da41463a2..1a471d3bd06 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -17,8 +17,8 @@ def load(): SolverFactory.register( - name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)' + name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver' )(Ipopt) SolverFactory.register( - name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' + name='gurobi', legacy_name='gurobi_v2', doc='Persistent interface to Gurobi' )(Gurobi) From f10ef5654828975d532354178f5fa7f96ac037d8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 15:22:32 -0600 Subject: [PATCH 0714/1178] Adding missing import --- pyomo/contrib/solver/gurobi_direct.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 56047b6c2c7..be06c17b63b 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -12,6 +12,7 @@ import datetime import io import math +import os from pyomo.common.config import ConfigValue from pyomo.common.dependencies import attempt_import From 7f0f3004a15baa555f9ae32e95c1ae6fa81b24c2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 16:05:41 -0600 Subject: [PATCH 0715/1178] Accept / ignore None in certain _map_config arguments --- pyomo/contrib/solver/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 9e0356c9c21..8840265763e 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -401,11 +401,11 @@ def _map_config( self.config.raise_exception_on_nonoptimal_result = ( raise_exception_on_nonoptimal_result ) - if solver_io is not NOTSET: + if solver_io is not NOTSET and solver_io is not None: raise NotImplementedError('Still working on this') - if suffixes is not NOTSET: + if suffixes is not NOTSET and suffixes is not None: raise NotImplementedError('Still working on this') - if logfile is not NOTSET: + if logfile is not NOTSET and logfile is not None: raise NotImplementedError('Still working on this') if keepfiles or 'keepfiles' in self.config: cwd = os.getcwd() From 2e210538774ab647486512f743a765f504a53f05 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 16:06:05 -0600 Subject: [PATCH 0716/1178] Update tests to track changes in the LegacySolverWrapper --- pyomo/contrib/solver/tests/solvers/test_ipopt.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index dc6bcf24855..d5d82981ed8 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -48,7 +48,7 @@ def test_ipopt_config(self): self.assertIsInstance(config.executable, ExecutableData) # Test custom initialization - solver = SolverFactory('ipopt_v2', executable='/path/to/exe') + solver = SolverFactory('ipopt', executable='/path/to/exe') self.assertFalse(solver.config.tee) self.assertTrue(solver.config.executable.startswith('/path')) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 74c495b86cc..5c138a6522b 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -178,7 +178,7 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): def test_class_method_list(self): - expected_list = ['available', 'license_is_valid', 'solve'] + expected_list = ['available', 'config_block', 'license_is_valid', 'set_options', 'solve'] method_list = [ method for method in dir(base.LegacySolverWrapper) @@ -207,9 +207,7 @@ def test_map_config(self): self.assertTrue(instance.config.tee) self.assertFalse(instance.config.load_solutions) self.assertEqual(instance.config.time_limit, 20) - # Report timing shouldn't be created because it no longer exists - with self.assertRaises(AttributeError): - print(instance.config.report_timing) + self.assertEqual(instance.config.report_timing, True) # Keepfiles should not be created because we did not declare keepfiles on # the original config with self.assertRaises(AttributeError): From 0465c89d94b58223694fed84334ce603c9d66f15 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 16:09:37 -0600 Subject: [PATCH 0717/1178] bugfix: correct option name --- pyomo/contrib/solver/gurobi_direct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index be06c17b63b..7b5ec6ed904 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -196,8 +196,8 @@ def solve(self, model, **kwds) -> Results: try: orig_cwd = os.getcwd() - if self._config.working_directory: - os.chdir(self._config.working_directory) + if self._config.working_dir: + os.chdir(self._config.working_dir) with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): gurobi_model = gurobipy.Model() From 9d1b91de17f843b5625cf1292df85db7d19d2985 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 21:27:27 -0600 Subject: [PATCH 0718/1178] NFC: apply black --- pyomo/contrib/solver/tests/unit/test_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 5c138a6522b..179d9823679 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -178,7 +178,13 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): def test_class_method_list(self): - expected_list = ['available', 'config_block', 'license_is_valid', 'set_options', 'solve'] + expected_list = [ + 'available', + 'config_block', + 'license_is_valid', + 'set_options', + 'solve', + ] method_list = [ method for method in dir(base.LegacySolverWrapper) From 9fd202e7b4bca74a11befbd422da925015216c60 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 22:58:58 -0600 Subject: [PATCH 0719/1178] update GDP baselines to reflect change in variable order? --- pyomo/gdp/tests/jobshop_large_hull.lp | 356 +++++++++++++------------- pyomo/gdp/tests/jobshop_small_hull.lp | 68 ++--- 2 files changed, 212 insertions(+), 212 deletions(-) diff --git a/pyomo/gdp/tests/jobshop_large_hull.lp b/pyomo/gdp/tests/jobshop_large_hull.lp index ee8ee0a73d2..f0a9d3ccbf0 100644 --- a/pyomo/gdp/tests/jobshop_large_hull.lp +++ b/pyomo/gdp/tests/jobshop_large_hull.lp @@ -66,17 +66,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: +1 t(C) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(6)_: +1 t(D) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ @@ -114,17 +114,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(11)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ @@ -150,29 +150,29 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(17)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(18)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: +1 t(C) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: +1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: +1 t(D) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(22)_: +1 t(D) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ @@ -186,17 +186,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(23)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(24)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(26)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ @@ -234,17 +234,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(31)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(32)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(34)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ @@ -294,17 +294,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(41)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(42)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(44)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ @@ -342,17 +342,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(49)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(50)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(52)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ @@ -390,17 +390,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(57)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(58)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(60)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ @@ -426,17 +426,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(63)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(64)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(66)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ @@ -701,34 +701,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +6.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ -92 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ -92 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +3.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ -92 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -92 NoClash(A_C_1_1)_binary_indicator_var <= 0 @@ -827,34 +827,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +2.0 NoClash(A_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ -92 NoClash(A_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ -92 NoClash(A_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +3.0 NoClash(A_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ -92 NoClash(A_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -92 NoClash(A_F_1_1)_binary_indicator_var <= 0 @@ -923,66 +923,66 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +9.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ -92 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ -92 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -3.0 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ -92 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -92 NoClash(B_C_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +8.0 NoClash(B_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ -92 NoClash(B_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ -92 NoClash(B_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +3.0 NoClash(B_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ -92 NoClash(B_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ -92 NoClash(B_D_2_1)_binary_indicator_var <= 0 @@ -1019,34 +1019,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +4.0 NoClash(B_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ -92 NoClash(B_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ -92 NoClash(B_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +3.0 NoClash(B_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ -92 NoClash(B_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ -92 NoClash(B_E_2_1)_binary_indicator_var <= 0 @@ -1146,34 +1146,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +8.0 NoClash(B_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ -92 NoClash(B_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ -92 NoClash(B_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +3.0 NoClash(B_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ -92 NoClash(B_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ -92 NoClash(B_G_2_1)_binary_indicator_var <= 0 @@ -1306,34 +1306,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(C)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +2.0 NoClash(C_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ -92 NoClash(C_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ -92 NoClash(C_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +6.0 NoClash(C_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ -92 NoClash(C_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ -92 NoClash(C_F_1_1)_binary_indicator_var <= 0 @@ -1434,34 +1434,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(C)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +4.0 NoClash(D_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ -92 NoClash(D_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ -92 NoClash(D_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +8.0 NoClash(D_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ -92 NoClash(D_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ -92 NoClash(D_E_2_1)_binary_indicator_var <= 0 @@ -1562,34 +1562,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(D)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +8.0 NoClash(D_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ -92 NoClash(D_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ -92 NoClash(D_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +8.0 NoClash(D_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ -92 NoClash(D_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ -92 NoClash(D_G_2_1)_binary_indicator_var <= 0 @@ -1657,34 +1657,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(E)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +8.0 NoClash(E_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ -92 NoClash(E_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ -92 NoClash(E_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +4.0 NoClash(E_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ -92 NoClash(E_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ -92 NoClash(E_G_2_1)_binary_indicator_var <= 0 @@ -1769,10 +1769,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ <= 92 @@ -1785,10 +1785,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ <= 92 @@ -1797,22 +1797,22 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ <= 92 @@ -1825,10 +1825,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(B)_ <= 92 @@ -1845,10 +1845,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ <= 92 @@ -1861,10 +1861,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ <= 92 @@ -1877,10 +1877,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 92 @@ -1889,10 +1889,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ <= 92 diff --git a/pyomo/gdp/tests/jobshop_small_hull.lp b/pyomo/gdp/tests/jobshop_small_hull.lp index ae2d738d29c..eccaa800600 100644 --- a/pyomo/gdp/tests/jobshop_small_hull.lp +++ b/pyomo/gdp/tests/jobshop_small_hull.lp @@ -34,29 +34,29 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ += 0 + +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +1 t(A) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ -= 0 - c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: +1 NoClash(A_B_3_0)_binary_indicator_var +1 NoClash(A_B_3_1)_binary_indicator_var @@ -104,66 +104,66 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +2.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +5.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +6.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ -19 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ -19 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +1 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ -19 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -19 NoClash(B_C_2_1)_binary_indicator_var <= 0 @@ -176,14 +176,14 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 From ae439ad8be5df810443fea62d1d0ba6cffe41feb Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:04:07 -0600 Subject: [PATCH 0720/1178] use get_vars_from_components in create_subsystem_block --- pyomo/util/subsystems.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 5789829ac54..4a9b96fa89b 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -14,7 +14,7 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.modeling import unique_component_name - +from pyomo.util.vars_from_expressions import get_vars_from_components from pyomo.core.base.constraint import Constraint from pyomo.core.base.expression import Expression from pyomo.core.base.objective import Objective @@ -131,12 +131,12 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): block.vars = Reference(variables) block.cons = Reference(constraints) var_set = ComponentSet(variables) - input_vars = [] - for con in constraints: - for var in identify_variables(con.expr, include_fixed=include_fixed): - if var not in var_set: - input_vars.append(var) - var_set.add(var) + input_vars = [ + var for var in get_vars_from_components( + block, Constraint, include_fixed=include_fixed + ) + if var not in var_set + ] block.input_vars = Reference(input_vars) add_local_external_functions(block) return block From be31a20946cd24a5a8b58ebff24992ea051427bf Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:09:52 -0600 Subject: [PATCH 0721/1178] formatting fix --- pyomo/util/subsystems.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 4a9b96fa89b..00c3b85ce47 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -131,12 +131,10 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): block.vars = Reference(variables) block.cons = Reference(constraints) var_set = ComponentSet(variables) - input_vars = [ - var for var in get_vars_from_components( - block, Constraint, include_fixed=include_fixed - ) - if var not in var_set - ] + input_vars = [] + for var in get_vars_from_components(block, Constraint, include_fixed=include_fixed): + if var not in var_set: + input_vars.append(var) block.input_vars = Reference(input_vars) add_local_external_functions(block) return block From 08b9d93c3635776948b6145b07e0b03dfde65c87 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:17:39 -0600 Subject: [PATCH 0722/1178] remove unused imports --- pyomo/util/vars_from_expressions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 62953af456b..878a1a13b58 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -17,8 +17,7 @@ actually in the subtree or not. """ from pyomo.core import Block -from pyomo.core.expr.visitor import _StreamVariableVisitor -from pyomo.core.expr import identify_variables +import pyomo.core.expr as EXPR def get_vars_from_components( @@ -52,7 +51,7 @@ def get_vars_from_components( descend_into=descend_into, descent_order=descent_order, ): - for var in identify_variables( + for var in EXPR.identify_variables( constraint.expr, include_fixed=include_fixed, named_expression_cache=named_expression_cache, From 443826da5ab3485a20439369b167c5e6a363ceff Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:22:16 -0600 Subject: [PATCH 0723/1178] remove old _VariableVisitor and rename new visitor to _VariableVisitor --- pyomo/core/expr/visitor.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 2fddca22c5f..08015f8b42c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1373,22 +1373,7 @@ def identify_components(expr, component_types): # ===================================================== -class _VariableVisitor(SimpleExpressionVisitor): - def __init__(self): - self.seen = set() - - def visit(self, node): - if node.__class__ in nonpyomo_leaf_types: - return - - if node.is_variable_type(): - if id(node) in self.seen: - return - self.seen.add(id(node)) - return node - - -class _StreamVariableVisitor(StreamBasedExpressionVisitor): +class _VariableVisitor(StreamBasedExpressionVisitor): def __init__(self, include_fixed=False, named_expression_cache=None): """Visitor that collects all unique variables participating in an expression @@ -1522,7 +1507,7 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ if named_expression_cache is None: named_expression_cache = {} - visitor = _StreamVariableVisitor( + visitor = _VariableVisitor( named_expression_cache=named_expression_cache, include_fixed=include_fixed ) variables = visitor.walk_expression(expr) From 3e7e7a2ad9559e3ffbfdeab2ca21932d55179d4a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 23:22:17 -0600 Subject: [PATCH 0724/1178] bugfix: update doc not solver name --- pyomo/contrib/solver/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 8861534bd01..99fbcc3a6d0 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -27,7 +27,7 @@ def decorator(cls): class LegacySolver(LegacySolverWrapper, cls): pass - LegacySolverFactory.register(legacy_name + " (new interface)", doc)( + LegacySolverFactory.register(legacy_name, doc + " (new interface)")( LegacySolver ) From db5d6dc96181f734f5f3410987ce4c0a1e3a6e7b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 20 Mar 2024 10:53:00 +0100 Subject: [PATCH 0725/1178] Fixed maingopy import --- pyomo/contrib/appsi/solvers/maingo.py | 246 +---------------- .../appsi/solvers/maingo_solvermodel.py | 257 ++++++++++++++++++ 2 files changed, 272 insertions(+), 231 deletions(-) create mode 100644 pyomo/contrib/appsi/solvers/maingo_solvermodel.py diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 614e12d227b..29464e6a876 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -25,7 +25,7 @@ from pyomo.core.base.expression import ScalarExpression from pyomo.core.base.param import _ParamData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import Var, ScalarVar, _GeneralVarData import pyomo.core.expr.expr_common as common import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import ( @@ -40,7 +40,18 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.util import valid_expr_ctypes_minlp -_plusMinusOne = {-1, 1} + +def _import_SolverModel(): + try: + from . import maingo_solvermodel + except ImportError: + raise + return maingo_solvermodel + + +maingo_solvermodel, solvermodel_available = attempt_import( + "maingo_solvermodel", importer=_import_SolverModel +) MaingoVar = namedtuple("MaingoVar", "type name lb ub init") @@ -103,234 +114,6 @@ def __init__(self, solver): self.solution_loader = MAiNGOSolutionLoader(solver=solver) -class SolverModel(maingopy.MAiNGOmodel): - def __init__(self, var_list, objective, con_list, idmap): - maingopy.MAiNGOmodel.__init__(self) - self._var_list = var_list - self._con_list = con_list - self._objective = objective - self._idmap = idmap - - def build_maingo_objective(self, obj, visitor): - maingo_obj = visitor.dfs_postorder_stack(obj.expr) - if obj.sense == maximize: - maingo_obj *= -1 - return maingo_obj - - def build_maingo_constraints(self, cons, visitor): - eqs = [] - ineqs = [] - for con in cons: - if con.equality: - eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] - elif con.has_ub() and con.has_lb(): - ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] - elif con.has_ub(): - ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - elif con.has_ub(): - ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] - else: - raise ValueError( - "Constraint does not have a lower " - "or an upper bound: {0} \n".format(con) - ) - return eqs, ineqs - - def get_variables(self): - return [ - maingopy.OptimizationVariable( - maingopy.Bounds(var.lb, var.ub), var.type, var.name - ) - for var in self._var_list - ] - - def get_initial_point(self): - return [ - var.init if not var.init is None else (var.lb + var.ub) / 2.0 - for var in self._var_list - ] - - def evaluate(self, maingo_vars): - visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) - result = maingopy.EvaluationContainer() - result.objective = self.build_maingo_objective(self._objective, visitor) - eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) - result.eq = eqs - result.ineq = ineqs - return result - - -LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT -RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT - - -class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): - def __init__(self, variables, idmap): - super(ToMAiNGOVisitor, self).__init__() - self.variables = variables - self.idmap = idmap - self._pyomo_func_to_maingo_func = { - "log": maingopy.log, - "log10": ToMAiNGOVisitor.maingo_log10, - "sin": maingopy.sin, - "cos": maingopy.cos, - "tan": maingopy.tan, - "cosh": maingopy.cosh, - "sinh": maingopy.sinh, - "tanh": maingopy.tanh, - "asin": maingopy.asin, - "acos": maingopy.acos, - "atan": maingopy.atan, - "exp": maingopy.exp, - "sqrt": maingopy.sqrt, - "asinh": ToMAiNGOVisitor.maingo_asinh, - "acosh": ToMAiNGOVisitor.maingo_acosh, - "atanh": ToMAiNGOVisitor.maingo_atanh, - } - - @classmethod - def maingo_log10(cls, x): - return maingopy.log(x) / math.log(10) - - @classmethod - def maingo_asinh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) - - @classmethod - def maingo_acosh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) - - @classmethod - def maingo_atanh(cls, x): - return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) - - def visit(self, node, values): - """Visit nodes that have been expanded""" - for i, val in enumerate(values): - arg = node._args_[i] - - if arg is None: - values[i] = "Undefined" - elif arg.__class__ in native_numeric_types: - pass - elif arg.__class__ in nonpyomo_leaf_types: - values[i] = val - else: - parens = False - if arg.is_expression_type() and node.PRECEDENCE is not None: - if arg.PRECEDENCE is None: - pass - elif node.PRECEDENCE < arg.PRECEDENCE: - parens = True - elif node.PRECEDENCE == arg.PRECEDENCE: - if i == 0: - parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT - elif i == len(node._args_) - 1: - parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT - else: - parens = True - if parens: - values[i] = val - - if node.__class__ in EXPR.NPV_expression_types: - return value(node) - - if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: - return values[0] * values[1] - - if node.__class__ in {EXPR.SumExpression}: - return sum(values) - - if node.__class__ in {EXPR.PowExpression}: - return maingopy.pow(values[0], values[1]) - - if node.__class__ in {EXPR.DivisionExpression}: - return values[0] / values[1] - - if node.__class__ in {EXPR.NegationExpression}: - return -values[0] - - if node.__class__ in {EXPR.AbsExpression}: - return maingopy.abs(values[0]) - - if node.__class__ in {EXPR.UnaryFunctionExpression}: - pyomo_func = node.getname() - maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] - return maingo_func(values[0]) - - if node.__class__ in {ScalarExpression}: - return values[0] - - raise ValueError(f"Unknown function expression encountered: {node.getname()}") - - def visiting_potential_leaf(self, node): - """ - Visiting a potential leaf. - - Return True if the node is not expanded. - """ - if node.__class__ in native_types: - return True, node - - if node.is_expression_type(): - if node.__class__ is EXPR.MonomialTermExpression: - return True, self._monomial_to_maingo(node) - if node.__class__ is EXPR.LinearExpression: - return True, self._linear_to_maingo(node) - return False, None - - if node.is_component_type(): - if node.ctype not in valid_expr_ctypes_minlp: - # Make sure all components in active constraints - # are basic ctypes we know how to deal with. - raise RuntimeError( - "Unallowable component '%s' of type %s found in an active " - "constraint or objective.\nMAiNGO cannot export " - "expressions with this component type." - % (node.name, node.ctype.__name__) - ) - - if node.is_fixed(): - return True, node() - else: - assert node.is_variable_type() - maingo_var_id = self.idmap[id(node)] - maingo_var = self.variables[maingo_var_id] - return True, maingo_var - - def _monomial_to_maingo(self, node): - const, var = node.args - maingo_var_id = self.idmap[id(var)] - maingo_var = self.variables[maingo_var_id] - if const.__class__ not in native_types: - const = value(const) - if var.is_fixed(): - return const * var.value - if not const: - return 0 - if const in _plusMinusOne: - if const < 0: - return -maingo_var - else: - return maingo_var - return const * maingo_var - - def _linear_to_maingo(self, node): - values = [ - ( - self._monomial_to_maingo(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() - ) - else value(arg) - ) - for arg in node.args - ] - return sum(values) - - class MAiNGO(PersistentBase, PersistentSolver): """ Interface to MAiNGO @@ -536,7 +319,8 @@ def set_instance(self, model): self._labeler = NumericLabeler("x") self.add_block(model) - self._solver_model = SolverModel( + + self._solver_model = maingo_solvermodel.SolverModel( var_list=self._maingo_vars, con_list=self._cons, objective=self._objective, diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py new file mode 100644 index 00000000000..4abc53ae290 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -0,0 +1,257 @@ +import math + +from pyomo.common.dependencies import attempt_import +from pyomo.core.base.var import ScalarVar +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.repn.util import valid_expr_ctypes_minlp + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + +_plusMinusOne = {1, -1} + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) + + @classmethod + def maingo_atanh(cls, x): + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + if node.__class__ is ScalarVar: + var = node + const = 1 + else: + const, var = node.args + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _linear_to_maingo(self, node): + values = [ + ( + self._monomial_to_maingo(arg) + if (arg.__class__ in {EXPR.MonomialTermExpression, ScalarVar}) + else (value(arg)) + ) + for arg in node.args + ] + return sum(values) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + + def build_maingo_objective(self, obj, visitor): + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + maingo_obj *= -1 + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + return [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + + def get_initial_point(self): + return [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result From 56b513dcf2ec852ce993400d3245b58cb46c5fc4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 15:38:29 -0600 Subject: [PATCH 0726/1178] add tests for mixed standard form --- pyomo/repn/tests/test_standard_form.py | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index c8b914deca5..9dee2b1d25d 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -42,6 +42,8 @@ def test_linear_model(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) def test_almost_dense_linear_model(self): m = pyo.ConcreteModel() @@ -55,6 +57,8 @@ def test_almost_dense_linear_model(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) def test_linear_model_row_col_order(self): m = pyo.ConcreteModel() @@ -70,6 +74,8 @@ def test_linear_model_row_col_order(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[4, 0, 1], [0, -1, -2]]))) self.assertTrue(np.all(repn.rhs == np.array([5, -3]))) + self.assertEqual(repn.rows, [(m.d, 1), (m.c, -1)]) + self.assertEqual(repn.columns, [m.y[3], m.x, m.y[1]]) def test_suffix_warning(self): m = pyo.ConcreteModel() @@ -235,6 +241,40 @@ def test_alternative_forms(self): ) self._verify_solution(soln, repn, True) + repn = LinearStandardFormCompiler().write( + m, mixed_form=True, column_order=col_order + ) + + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]) + self.assertEqual( + list(map(str, repn.x)), + ['x', 'y[0]', 'y[1]', 'y[3]'], + ) + self.assertEqual( + list(v.bounds for v in repn.x), + [(None, None), (0, 10), (-5, 10), (-5, -2)], + ) + ref = np.array( + [ + [1, 0, 2, 0], + [0, 0, 1, 4], + [0, 1, 6, 0], + [0, 1, 6, 0], + [1, 1, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + print(repn) + print(repn.b) + self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) + self.assertTrue( + np.all( + repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]) + ) + ) + # Note that the solution is a mix of inequality and equality constraints + # self._verify_solution(soln, repn, False) + repn = LinearStandardFormCompiler().write( m, slack_form=True, nonnegative_vars=True, column_order=col_order ) From a0b9a927e4997b767b267bba19d3598d561c2b8b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:20:15 -0600 Subject: [PATCH 0727/1178] Renamed _ArcData -> ArcData --- pyomo/core/base/component.py | 2 +- pyomo/network/arc.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 22c2bc4b804..c91167379fd 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -806,7 +806,7 @@ class ComponentData(_ComponentBase): # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, - # _ArcData, _PortData, _LinearConstraintData, and + # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! def __init__(self, component): diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index 42b7c6ea075..5e68f181a38 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -52,7 +52,7 @@ def _iterable_to_dict(vals, directed, name): return vals -class _ArcData(ActiveComponentData): +class ArcData(ActiveComponentData): """ This class defines the data for a single Arc @@ -246,6 +246,11 @@ def _validate_ports(self, source, destination, ports): ) +class _ArcData(metaclass=RenamedClass): + __renamed__new_class__ = ArcData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Component used for connecting two Ports.") class Arc(ActiveIndexedComponent): """ @@ -267,7 +272,7 @@ class Arc(ActiveIndexedComponent): or a two-member iterable of ports """ - _ComponentDataClass = _ArcData + _ComponentDataClass = ArcData def __new__(cls, *args, **kwds): if cls != Arc: @@ -373,9 +378,9 @@ def _pprint(self): ) -class ScalarArc(_ArcData, Arc): +class ScalarArc(ArcData, Arc): def __init__(self, *args, **kwds): - _ArcData.__init__(self, self) + ArcData.__init__(self, self) Arc.__init__(self, *args, **kwds) self.index = UnindexedComponent_index From 2f3e94039bfffac5da785978b0c7bf8c6aa20c9e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:31:31 -0600 Subject: [PATCH 0728/1178] Renamed _BlockData -> BlockData --- pyomo/contrib/appsi/base.py | 12 ++-- pyomo/contrib/appsi/fbbt.py | 6 +- pyomo/contrib/appsi/solvers/cbc.py | 6 +- pyomo/contrib/appsi/solvers/cplex.py | 6 +- pyomo/contrib/appsi/solvers/ipopt.py | 6 +- pyomo/contrib/appsi/solvers/wntr.py | 4 +- pyomo/contrib/appsi/writers/lp_writer.py | 4 +- pyomo/contrib/appsi/writers/nl_writer.py | 4 +- pyomo/contrib/benders/benders_cuts.py | 6 +- pyomo/contrib/cp/interval_var.py | 6 +- .../logical_to_disjunctive_program.py | 4 +- pyomo/contrib/incidence_analysis/interface.py | 6 +- .../contrib/incidence_analysis/scc_solver.py | 4 +- .../tests/test_interface.py | 2 +- pyomo/contrib/latex_printer/latex_printer.py | 6 +- .../piecewise/piecewise_linear_function.py | 6 +- .../piecewise_to_gdp_transformation.py | 4 +- .../pynumero/interfaces/external_grey_box.py | 6 +- pyomo/contrib/solver/base.py | 14 ++-- pyomo/contrib/viewer/report.py | 2 +- pyomo/core/base/block.py | 65 ++++++++++--------- pyomo/core/base/piecewise.py | 6 +- .../plugins/transform/logical_to_linear.py | 4 +- pyomo/core/tests/unit/test_block.py | 14 ++-- pyomo/core/tests/unit/test_component.py | 8 +-- pyomo/core/tests/unit/test_indexed_slice.py | 4 +- pyomo/core/tests/unit/test_suffix.py | 4 +- pyomo/dae/flatten.py | 8 +-- pyomo/gdp/disjunct.py | 8 +-- pyomo/gdp/plugins/gdp_var_mover.py | 2 +- pyomo/gdp/tests/common_tests.py | 10 +-- pyomo/gdp/transformed_disjunct.py | 6 +- pyomo/gdp/util.py | 8 +-- pyomo/mpec/complementarity.py | 4 +- pyomo/opt/base/solvers.py | 8 +-- pyomo/repn/util.py | 4 +- .../solvers/direct_or_persistent_solver.py | 4 +- .../solvers/plugins/solvers/direct_solver.py | 8 +-- pyomo/solvers/plugins/solvers/mosek_direct.py | 2 +- .../plugins/solvers/persistent_solver.py | 8 +-- pyomo/util/report_scaling.py | 8 +-- pyomo/util/slices.py | 2 +- 42 files changed, 156 insertions(+), 153 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..b4ade16a597 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -25,7 +25,7 @@ from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective @@ -621,13 +621,13 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: """ Solve a Pyomo model. Parameters ---------- - model: _BlockData + model: BlockData The Pyomo model to be solved timer: HierarchicalTimer An option timer for reporting timing @@ -811,7 +811,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): pass @abc.abstractmethod - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): pass @abc.abstractmethod @@ -827,7 +827,7 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): pass @abc.abstractmethod - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): pass @abc.abstractmethod @@ -1529,7 +1529,7 @@ def update(self, timer: HierarchicalTimer = None): class LegacySolverInterface(object): def solve( self, - model: _BlockData, + model: BlockData, tee: bool = False, load_solutions: bool = True, logfile: Optional[str] = None, diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 8b6cc52d2aa..c6bbdb5bf3b 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -23,7 +23,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -275,7 +275,7 @@ def _deactivate_satisfied_cons(self): c.deactivate() def perform_fbbt( - self, model: _BlockData, symbolic_solver_labels: Optional[bool] = None + self, model: BlockData, symbolic_solver_labels: Optional[bool] = None ): if model is not self._model: self.set_instance(model, symbolic_solver_labels=symbolic_solver_labels) @@ -304,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: _BlockData, seed_var: _GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: _GeneralVarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 7f04ffbfce7..57bbf1b4c21 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -28,7 +28,7 @@ from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer @@ -173,7 +173,7 @@ def add_params(self, params: List[_ParamData]): def add_constraints(self, cons: List[_GeneralConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) def remove_variables(self, variables: List[_GeneralVarData]): @@ -185,7 +185,7 @@ def remove_params(self, params: List[_ParamData]): def remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) def set_objective(self, obj: _GeneralObjectiveData): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1b7ab5000d2..2e04a979fda 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -24,7 +24,7 @@ from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer @@ -188,7 +188,7 @@ def add_params(self, params: List[_ParamData]): def add_constraints(self, cons: List[_GeneralConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) def remove_variables(self, variables: List[_GeneralVarData]): @@ -200,7 +200,7 @@ def remove_params(self, params: List[_ParamData]): def remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) def set_objective(self, obj: _GeneralObjectiveData): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 54e21d333e5..19ec5f8031c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -30,7 +30,7 @@ from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer @@ -237,7 +237,7 @@ def add_params(self, params: List[_ParamData]): def add_constraints(self, cons: List[_GeneralConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) def remove_variables(self, variables: List[_GeneralVarData]): @@ -249,7 +249,7 @@ def remove_params(self, params: List[_ParamData]): def remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) def set_objective(self, obj: _GeneralObjectiveData): diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 00c0598c687..928eda2b514 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -39,7 +39,7 @@ from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import native_numeric_types from typing import Dict, Optional, List -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import _GeneralConstraintData @@ -178,7 +178,7 @@ def _solve(self, timer: HierarchicalTimer): ) return results - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: StaleFlagManager.mark_all_as_stale() if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 9984cb7465d..518be5fac99 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -15,7 +15,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value from pyomo.contrib.appsi.base import PersistentBase @@ -167,7 +167,7 @@ def _set_objective(self, obj: _GeneralObjectiveData): cobj.name = cname self._writer.objective = cobj - def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = None): + def write(self, model: BlockData, filename: str, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() if model is not self._model: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index bd24a86216a..75b026ab521 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -15,7 +15,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value from pyomo.contrib.appsi.base import PersistentBase @@ -232,7 +232,7 @@ def _set_objective(self, obj: _GeneralObjectiveData): cobj.sense = sense self._writer.objective = cobj - def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = None): + def write(self, model: BlockData, filename: str, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() if model is not self._model: diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index cf96ba26164..0653be55986 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.core.base.block import _BlockData, declare_custom_block +from pyomo.core.base.block import BlockData, declare_custom_block import pyomo.environ as pyo from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver from pyomo.core.expr.visitor import identify_variables @@ -166,13 +166,13 @@ def _setup_subproblem(b, root_vars, relax_subproblem_cons): @declare_custom_block(name='BendersCutGenerator') -class BendersCutGeneratorData(_BlockData): +class BendersCutGeneratorData(BlockData): def __init__(self, component): if not mpi4py_available: raise ImportError('BendersCutGenerator requires mpi4py.') if not numpy_available: raise ImportError('BendersCutGenerator requires numpy.') - _BlockData.__init__(self, component) + BlockData.__init__(self, component) self.num_subproblems_by_rank = 0 # np.zeros(self.comm.Get_size()) self.subproblems = list() diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 953b859ea20..ff11d6e3a9f 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -18,7 +18,7 @@ from pyomo.core import Integers, value from pyomo.core.base import Any, ScalarVar, ScalarBooleanVar -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set @@ -87,14 +87,14 @@ def get_associated_interval_var(self): return self.parent_block() -class IntervalVarData(_BlockData): +class IntervalVarData(BlockData): """This class defines the abstract interface for a single interval variable.""" # We will put our four variables on this, and everything else is off limits. _Block_reserved_words = Any def __init__(self, component=None): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self.is_present = IntervalVarPresence() diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py index e318e621e88..c29bf3f2675 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py @@ -26,7 +26,7 @@ Transformation, NonNegativeIntegers, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base import SortComponents from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction @@ -73,7 +73,7 @@ def _apply_to(self, model, **kwds): transBlocks = {} visitor = LogicalToDisjunctiveVisitor() for t in targets: - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, model, visitor, transBlocks) elif t.ctype is LogicalConstraint: if t.is_indexed(): diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 50cb84daaf5..b798dafced7 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -15,7 +15,7 @@ import enum import textwrap -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.var import Var from pyomo.core.base.constraint import Constraint from pyomo.core.base.objective import Objective @@ -279,7 +279,7 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): self._incidence_graph = None self._variables = None self._constraints = None - elif isinstance(model, _BlockData): + elif isinstance(model, BlockData): self._constraints = [ con for con in model.component_data_objects(Constraint, active=active) @@ -348,7 +348,7 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): else: raise TypeError( "Unsupported type for incidence graph. Expected PyomoNLP" - " or _BlockData but got %s." % type(model) + " or BlockData but got %s." % type(model) ) @property diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 0c59fe8703e..378647c190c 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -27,7 +27,7 @@ def generate_strongly_connected_components( constraints, variables=None, include_fixed=False, igraph=None ): - """Yield in order ``_BlockData`` that each contain the variables and + """Yield in order ``BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization of the incidence matrix of constraints and variables @@ -51,7 +51,7 @@ def generate_strongly_connected_components( Yields ------ - Tuple of ``_BlockData``, list-of-variables + Tuple of ``BlockData``, list-of-variables Blocks containing the variables and constraints of every strongly connected component, in a topological order. The variables are the "input variables" for that block. diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 4b77d60d8ba..117e2e53b6d 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1888,7 +1888,7 @@ def test_block_data_obj(self): self.assertEqual(len(var_dmp.unmatched), 1) self.assertEqual(len(con_dmp.unmatched), 1) - msg = "Unsupported type.*_BlockData" + msg = "Unsupported type.*BlockData" with self.assertRaisesRegex(TypeError, msg): igraph = IncidenceGraphInterface(m.block) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 0a595dd8e1b..42fc9083953 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -64,7 +64,7 @@ from pyomo.core.base.external import _PythonCallbackFunctionID from pyomo.core.base.enums import SortComponents -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn.util import ExprType @@ -587,7 +587,7 @@ def latex_printer( Parameters ---------- - pyomo_component: _BlockData or Model or Objective or Constraint or Expression + pyomo_component: BlockData or Model or Objective or Constraint or Expression The Pyomo component to be printed latex_component_map: pyomo.common.collections.component_map.ComponentMap @@ -674,7 +674,7 @@ def latex_printer( use_equation_environment = True isSingle = True - elif isinstance(pyomo_component, _BlockData): + elif isinstance(pyomo_component, BlockData): objectives = [ obj for obj in pyomo_component.component_data_objects( diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 66ca02ad125..e92edacc756 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -20,7 +20,7 @@ PiecewiseLinearExpression, ) from pyomo.core import Any, NonNegativeIntegers, value, Var -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.expression import Expression from pyomo.core.base.global_set import UnindexedComponent_index @@ -36,11 +36,11 @@ logger = logging.getLogger(__name__) -class PiecewiseLinearFunctionData(_BlockData): +class PiecewiseLinearFunctionData(BlockData): _Block_reserved_words = Any def __init__(self, component=None): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self._expressions = Expression(NonNegativeIntegers) diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 2e056c47a15..779bb601c71 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -33,7 +33,7 @@ Any, ) from pyomo.core.base import Transformation -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction from pyomo.gdp.util import is_child_of @@ -147,7 +147,7 @@ def _apply_to_impl(self, instance, **kwds): self._transform_piecewise_linear_function( t, config.descend_into_expressions ) - elif t.ctype is Block or isinstance(t, _BlockData): + elif t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, config.descend_into_expressions) elif t.ctype is Constraint: if not config.descend_into_expressions: diff --git a/pyomo/contrib/pynumero/interfaces/external_grey_box.py b/pyomo/contrib/pynumero/interfaces/external_grey_box.py index 7e42f161bee..68e652575cc 100644 --- a/pyomo/contrib/pynumero/interfaces/external_grey_box.py +++ b/pyomo/contrib/pynumero/interfaces/external_grey_box.py @@ -18,7 +18,7 @@ from pyomo.common.log import is_debug_set from pyomo.common.timing import ConstructionTimer from pyomo.core.base import Var, Set, Constraint, value -from pyomo.core.base.block import _BlockData, Block, declare_custom_block +from pyomo.core.base.block import BlockData, Block, declare_custom_block from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.initializer import Initializer from pyomo.core.base.set import UnindexedComponent_set @@ -316,7 +316,7 @@ def evaluate_jacobian_outputs(self): # -class ExternalGreyBoxBlockData(_BlockData): +class ExternalGreyBoxBlockData(BlockData): def set_external_model(self, external_grey_box_model, inputs=None, outputs=None): """ Parameters @@ -424,7 +424,7 @@ class ScalarExternalGreyBoxBlock(ExternalGreyBoxBlockData, ExternalGreyBoxBlock) def __init__(self, *args, **kwds): ExternalGreyBoxBlockData.__init__(self, component=self) ExternalGreyBoxBlock.__init__(self, *args, **kwds) - # The above inherit from Block and _BlockData, so it's not until here + # The above inherit from Block and BlockData, so it's not until here # that we know it's scalar. So we set the index accordingly. self._index = UnindexedComponent_index diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8840265763e..4b7da383a57 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -17,7 +17,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError @@ -108,13 +108,13 @@ def __str__(self): @document_kwargs_from_configdict(CONFIG) @abc.abstractmethod - def solve(self, model: _BlockData, **kwargs) -> Results: + def solve(self, model: BlockData, **kwargs) -> Results: """ Solve a Pyomo model. Parameters ---------- - model: _BlockData + model: BlockData The Pyomo model to be solved **kwargs Additional keyword arguments (including solver_options - passthrough @@ -182,7 +182,7 @@ class PersistentSolverBase(SolverBase): @document_kwargs_from_configdict(PersistentSolverConfig()) @abc.abstractmethod - def solve(self, model: _BlockData, **kwargs) -> Results: + def solve(self, model: BlockData, **kwargs) -> Results: super().solve(model, kwargs) def is_persistent(self): @@ -300,7 +300,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): """ @abc.abstractmethod - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): """ Add a block to the model """ @@ -324,7 +324,7 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): """ @abc.abstractmethod - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): """ Remove a block from the model """ @@ -496,7 +496,7 @@ def _solution_handler( def solve( self, - model: _BlockData, + model: BlockData, tee: bool = False, load_solutions: bool = True, logfile: Optional[str] = None, diff --git a/pyomo/contrib/viewer/report.py b/pyomo/contrib/viewer/report.py index f83a53c608d..a1f893bba31 100644 --- a/pyomo/contrib/viewer/report.py +++ b/pyomo/contrib/viewer/report.py @@ -149,7 +149,7 @@ def degrees_of_freedom(blk): Return the degrees of freedom. Args: - blk (Block or _BlockData): Block to count degrees of freedom in + blk (Block or BlockData): Block to count degrees of freedom in Returns: (int): Number of degrees of freedom """ diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 2918ef78b00..8f4e86fe697 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -254,7 +254,7 @@ class _BlockConstruction(object): class PseudoMap(AutoSlots.Mixin): """ This class presents a "mock" dict interface to the internal - _BlockData data structures. We return this object to the + BlockData data structures. We return this object to the user to preserve the historical "{ctype : {name : obj}}" interface without actually regenerating that dict-of-dicts data structure. @@ -487,7 +487,7 @@ def iteritems(self): return self.items() -class _BlockData(ActiveComponentData): +class BlockData(ActiveComponentData): """ This class holds the fundamental block data. """ @@ -537,9 +537,9 @@ def __init__(self, component): # _ctypes: { ctype -> [1st idx, last idx, count] } # _decl: { name -> idx } # _decl_order: list( tuples( obj, next_type_idx ) ) - super(_BlockData, self).__setattr__('_ctypes', {}) - super(_BlockData, self).__setattr__('_decl', {}) - super(_BlockData, self).__setattr__('_decl_order', []) + super(BlockData, self).__setattr__('_ctypes', {}) + super(BlockData, self).__setattr__('_decl', {}) + super(BlockData, self).__setattr__('_decl_order', []) self._private_data = None def __getattr__(self, val) -> Union[Component, IndexedComponent, Any]: @@ -574,7 +574,7 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): # Other Python objects are added with the standard __setattr__ # method. # - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # Case 2. The attribute exists and it is a component in the # list of declarations in this block. We will use the @@ -628,11 +628,11 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): # else: # - # NB: This is important: the _BlockData is either a scalar + # NB: This is important: the BlockData is either a scalar # Block (where _parent and _component are defined) or a # single block within an Indexed Block (where only # _component is defined). Regardless, the - # _BlockData.__init__() method declares these methods and + # BlockData.__init__() method declares these methods and # sets them either to None or a weakref. Thus, we will # never have a problem converting these objects from # weakrefs into Blocks and back (when pickling); the @@ -647,23 +647,23 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): # return True, this shouldn't be too inefficient. # if name == '_parent': - if val is not None and not isinstance(val(), _BlockData): + if val is not None and not isinstance(val(), BlockData): raise ValueError( "Cannot set the '_parent' attribute of Block '%s' " "to a non-Block object (with type=%s); Did you " "try to create a model component named '_parent'?" % (self.name, type(val)) ) - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) elif name == '_component': - if val is not None and not isinstance(val(), _BlockData): + if val is not None and not isinstance(val(), BlockData): raise ValueError( "Cannot set the '_component' attribute of Block '%s' " "to a non-Block object (with type=%s); Did you " "try to create a model component named '_component'?" % (self.name, type(val)) ) - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # At this point, we should only be seeing non-component data # the user is hanging on the blocks (uncommon) or the @@ -680,7 +680,7 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): delattr(self, name) self.add_component(name, val) else: - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) def __delattr__(self, name): """ @@ -703,7 +703,7 @@ def __delattr__(self, name): # Other Python objects are removed with the standard __detattr__ # method. # - super(_BlockData, self).__delattr__(name) + super(BlockData, self).__delattr__(name) def _compact_decl_storage(self): idxMap = {} @@ -775,11 +775,11 @@ def transfer_attributes_from(self, src): Parameters ---------- - src: _BlockData or dict + src: BlockData or dict The Block or mapping that contains the new attributes to assign to this block. """ - if isinstance(src, _BlockData): + if isinstance(src, BlockData): # There is a special case where assigning a parent block to # this block creates a circular hierarchy if src is self: @@ -788,7 +788,7 @@ def transfer_attributes_from(self, src): while p_block is not None: if p_block is src: raise ValueError( - "_BlockData.transfer_attributes_from(): Cannot set a " + "BlockData.transfer_attributes_from(): Cannot set a " "sub-block (%s) to a parent block (%s): creates a " "circular hierarchy" % (self, src) ) @@ -804,7 +804,7 @@ def transfer_attributes_from(self, src): del_src_comp = lambda x: None else: raise ValueError( - "_BlockData.transfer_attributes_from(): expected a " + "BlockData.transfer_attributes_from(): expected a " "Block or dict; received %s" % (type(src).__name__,) ) @@ -878,7 +878,7 @@ def collect_ctypes(self, active=None, descend_into=True): def model(self): # - # Special case: the "Model" is always the top-level _BlockData, + # Special case: the "Model" is always the top-level BlockData, # so if this is the top-level block, it must be the model # # Also note the interesting and intentional characteristic for @@ -1035,7 +1035,7 @@ def add_component(self, name, val): # is inappropriate here. The correct way to add the attribute # is to delegate the work to the next class up the MRO. # - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # Update the ctype linked lists # @@ -1106,7 +1106,7 @@ def add_component(self, name, val): # This is tricky: If we are in the middle of # constructing an indexed block, the block component # already has _constructed=True. Now, if the - # _BlockData.__init__() defines any local variables + # BlockData.__init__() defines any local variables # (like pyomo.gdp.Disjunct's indicator_var), name(True) # will fail: this block data exists and has a parent(), # but it has not yet been added to the parent's _data @@ -1194,7 +1194,7 @@ def del_component(self, name_or_object): # Note: 'del self.__dict__[name]' is inappropriate here. The # correct way to add the attribute is to delegate the work to # the next class up the MRO. - super(_BlockData, self).__delattr__(name) + super(BlockData, self).__delattr__(name) def reclassify_component_type( self, name_or_object, new_ctype, preserve_declaration_order=True @@ -1994,6 +1994,11 @@ def private_data(self, scope=None): return self._private_data[scope] +class _BlockData(metaclass=RenamedClass): + __renamed__new_class__ = BlockData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A component that contains one or more model components." ) @@ -2007,7 +2012,7 @@ class Block(ActiveIndexedComponent): is deferred. """ - _ComponentDataClass = _BlockData + _ComponentDataClass = BlockData _private_data_initializers = defaultdict(lambda: dict) @overload @@ -2100,7 +2105,7 @@ def _getitem_when_not_present(self, idx): # components declared by the rule have the opportunity # to be initialized with data from # _BlockConstruction.data as they are transferred over. - if obj is not _block and isinstance(obj, _BlockData): + if obj is not _block and isinstance(obj, BlockData): _block.transfer_attributes_from(obj) finally: if data is not None and _block is not self: @@ -2221,7 +2226,7 @@ def display(self, filename=None, ostream=None, prefix=""): ostream = sys.stdout for key in sorted(self): - _BlockData.display(self[key], filename, ostream, prefix) + BlockData.display(self[key], filename, ostream, prefix) @staticmethod def register_private_data_initializer(initializer, scope=None): @@ -2241,9 +2246,9 @@ def register_private_data_initializer(initializer, scope=None): Block._private_data_initializers[scope] = initializer -class ScalarBlock(_BlockData, Block): +class ScalarBlock(BlockData, Block): def __init__(self, *args, **kwds): - _BlockData.__init__(self, component=self) + BlockData.__init__(self, component=self) Block.__init__(self, *args, **kwds) # Initialize the data dict so that (abstract) attribute # assignment will work. Note that we do not trigger @@ -2266,7 +2271,7 @@ def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) @overload - def __getitem__(self, index) -> _BlockData: ... + def __getitem__(self, index) -> BlockData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore @@ -2325,7 +2330,7 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F # Create a Block and record all the default attributes, methods, etc. # These will be assumed to be the set of illegal component names. # -_BlockData._Block_reserved_words = set(dir(Block())) +BlockData._Block_reserved_words = set(dir(Block())) class _IndexedCustomBlockMeta(type): @@ -2376,7 +2381,7 @@ def declare_custom_block(name, new_ctype=None): """Decorator to declare components for a custom block data class >>> @declare_custom_block(name=FooBlock) - ... class FooBlockData(_BlockData): + ... class FooBlockData(BlockData): ... # custom block data class ... pass """ diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index 7817a61b2f2..b15def13ccb 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -43,7 +43,7 @@ from pyomo.common.deprecation import deprecation_warning from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer -from pyomo.core.base.block import Block, _BlockData +from pyomo.core.base.block import Block, BlockData from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.constraint import Constraint, ConstraintList from pyomo.core.base.sos import SOSConstraint @@ -214,14 +214,14 @@ def _characterize_function(name, tol, f_rule, model, points, *index): return 0, values, False -class _PiecewiseData(_BlockData): +class _PiecewiseData(BlockData): """ This class defines the base class for all linearization and piecewise constraint generators.. """ def __init__(self, parent): - _BlockData.__init__(self, parent) + BlockData.__init__(self, parent) self._constructed = True self._bound_type = None self._domain_pts = None diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index 7aa541a5fdd..69328032004 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -29,7 +29,7 @@ BooleanVarList, SortComponents, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.boolean_var import _DeprecatedImplicitAssociatedBinaryVariable from pyomo.core.expr.cnf_walker import to_cnf from pyomo.core.expr import ( @@ -100,7 +100,7 @@ def _apply_to(self, model, **kwds): # the GDP will be solved, and it would be wrong to assume that a GDP # will *necessarily* be solved as an algebraic model. The star # example of not doing so being GDPopt.) - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, model, new_var_lists, transBlocks) elif t.ctype is LogicalConstraint: if t.is_indexed(): diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 71e80d90a73..660f65f1944 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -54,7 +54,7 @@ from pyomo.core.base.block import ( ScalarBlock, SubclassOf, - _BlockData, + BlockData, declare_custom_block, ) import pyomo.core.expr as EXPR @@ -851,7 +851,7 @@ class DerivedBlock(ScalarBlock): _Block_reserved_words = None DerivedBlock._Block_reserved_words = ( - set(['a', 'b', 'c']) | _BlockData._Block_reserved_words + set(['a', 'b', 'c']) | BlockData._Block_reserved_words ) m = ConcreteModel() @@ -965,7 +965,7 @@ def __init__(self, *args, **kwds): b.c.d.e = Block() with self.assertRaisesRegex( ValueError, - r'_BlockData.transfer_attributes_from\(\): ' + r'BlockData.transfer_attributes_from\(\): ' r'Cannot set a sub-block \(c.d.e\) to a parent block \(c\):', ): b.c.d.e.transfer_attributes_from(b.c) @@ -974,7 +974,7 @@ def __init__(self, *args, **kwds): b = Block(concrete=True) with self.assertRaisesRegex( ValueError, - r'_BlockData.transfer_attributes_from\(\): expected a Block ' + r'BlockData.transfer_attributes_from\(\): expected a Block ' 'or dict; received str', ): b.transfer_attributes_from('foo') @@ -2977,7 +2977,7 @@ def test_write_exceptions(self): def test_override_pprint(self): @declare_custom_block('TempBlock') - class TempBlockData(_BlockData): + class TempBlockData(BlockData): def pprint(self, ostream=None, verbose=False, prefix=""): ostream.write('Testing pprint of a custom block.') @@ -3052,9 +3052,9 @@ def test_derived_block_construction(self): class ConcreteBlock(Block): pass - class ScalarConcreteBlock(_BlockData, ConcreteBlock): + class ScalarConcreteBlock(BlockData, ConcreteBlock): def __init__(self, *args, **kwds): - _BlockData.__init__(self, component=self) + BlockData.__init__(self, component=self) ConcreteBlock.__init__(self, *args, **kwds) _buf = [] diff --git a/pyomo/core/tests/unit/test_component.py b/pyomo/core/tests/unit/test_component.py index 175c4c47d46..b12db9af047 100644 --- a/pyomo/core/tests/unit/test_component.py +++ b/pyomo/core/tests/unit/test_component.py @@ -66,19 +66,17 @@ def test_getname(self): ) m.b[2]._component = None - self.assertEqual( - m.b[2].getname(fully_qualified=True), "[Unattached _BlockData]" - ) + self.assertEqual(m.b[2].getname(fully_qualified=True), "[Unattached BlockData]") # I think that getname() should do this: # self.assertEqual(m.b[2].c[2,4].getname(fully_qualified=True), - # "[Unattached _BlockData].c[2,4]") + # "[Unattached BlockData].c[2,4]") # but it doesn't match current behavior. I will file a PEP to # propose changing the behavior later and proceed to test # current behavior. self.assertEqual(m.b[2].c[2, 4].getname(fully_qualified=True), "c[2,4]") self.assertEqual( - m.b[2].getname(fully_qualified=False), "[Unattached _BlockData]" + m.b[2].getname(fully_qualified=False), "[Unattached BlockData]" ) self.assertEqual(m.b[2].c[2, 4].getname(fully_qualified=False), "c[2,4]") diff --git a/pyomo/core/tests/unit/test_indexed_slice.py b/pyomo/core/tests/unit/test_indexed_slice.py index babd3f3c46a..40aaad9fec9 100644 --- a/pyomo/core/tests/unit/test_indexed_slice.py +++ b/pyomo/core/tests/unit/test_indexed_slice.py @@ -17,7 +17,7 @@ import pyomo.common.unittest as unittest from pyomo.environ import Var, Block, ConcreteModel, RangeSet, Set, Any -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.set import normalize_index @@ -64,7 +64,7 @@ def tearDown(self): self.m = None def test_simple_getitem(self): - self.assertIsInstance(self.m.b[1, 4], _BlockData) + self.assertIsInstance(self.m.b[1, 4], BlockData) def test_simple_getslice(self): _slicer = self.m.b[:, 4] diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index d2e861cceb5..9597bad7571 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1603,7 +1603,7 @@ def test_clone_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_clone_BlockData(self): + def test_cloneBlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() @@ -1761,7 +1761,7 @@ def test_pickle_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_pickle_BlockData(self): + def test_pickleBlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() diff --git a/pyomo/dae/flatten.py b/pyomo/dae/flatten.py index febaf7c10c9..3d90cc443c1 100644 --- a/pyomo/dae/flatten.py +++ b/pyomo/dae/flatten.py @@ -259,7 +259,7 @@ def generate_sliced_components( Parameters ---------- - b: _BlockData + b: BlockData Block whose components will be sliced index_stack: list @@ -267,7 +267,7 @@ def generate_sliced_components( component, that have been sliced. This is necessary to return the sets that have been sliced. - slice_: IndexedComponent_slice or _BlockData + slice_: IndexedComponent_slice or BlockData Slice generated so far. This function will yield extensions to this slice at the current level of the block hierarchy. @@ -443,7 +443,7 @@ def flatten_components_along_sets(m, sets, ctype, indices=None, active=None): Parameters ---------- - m: _BlockData + m: BlockData Block whose components (and their sub-components) will be partitioned @@ -546,7 +546,7 @@ def flatten_dae_components(model, time, ctype, indices=None, active=None): Parameters ---------- - model: _BlockData + model: BlockData Block whose components are partitioned time: Set diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index d6e5fcfec57..e6d8d709425 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -41,7 +41,7 @@ ComponentData, ) from pyomo.core.base.global_set import UnindexedComponent_index -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.base.indexed_component import ActiveIndexedComponent from pyomo.core.expr.expr_common import ExpressionType @@ -412,7 +412,7 @@ def process(arg): return (_Initializer.deferred_value, arg) -class _DisjunctData(_BlockData): +class _DisjunctData(BlockData): __autoslot_mappers__ = {'_transformation_block': AutoSlots.weakref_mapper} _Block_reserved_words = set() @@ -424,7 +424,7 @@ def transformation_block(self): ) def __init__(self, component): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self.indicator_var = AutoLinkedBooleanVar() self.binary_indicator_var = AutoLinkedBinaryVar(self.indicator_var) @@ -498,7 +498,7 @@ def _activate_without_unfixing_indicator(self): class ScalarDisjunct(_DisjunctData, Disjunct): def __init__(self, *args, **kwds): ## FIXME: This is a HACK to get around a chicken-and-egg issue - ## where _BlockData creates the indicator_var *before* + ## where BlockData creates the indicator_var *before* ## Block.__init__ declares the _defer_construction flag. self._defer_construction = True self._suppress_ctypes = set() diff --git a/pyomo/gdp/plugins/gdp_var_mover.py b/pyomo/gdp/plugins/gdp_var_mover.py index 5402b576368..7b1df0bb68f 100644 --- a/pyomo/gdp/plugins/gdp_var_mover.py +++ b/pyomo/gdp/plugins/gdp_var_mover.py @@ -115,7 +115,7 @@ def _apply_to(self, instance, **kwds): disjunct_component, Block ) # HACK: activate the block, but do not activate the - # _BlockData objects + # BlockData objects super(ActiveIndexedComponent, disjunct_component).activate() # Deactivate all constraints. Note that we only need to diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 5d0d6f6c21b..e15a7c66d8a 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -30,7 +30,7 @@ from pyomo.gdp import Disjunct, Disjunction, GDP_Error from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.core.base import constraint, ComponentUID -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR import pyomo.gdp.tests.models as models @@ -1704,10 +1704,10 @@ def check_all_components_transformed(self, m): # makeNestedDisjunctions_NestedDisjuncts model. self.assertIsInstance(m.disj.algebraic_constraint, Constraint) self.assertIsInstance(m.d1.disj2.algebraic_constraint, Constraint) - self.assertIsInstance(m.d1.transformation_block, _BlockData) - self.assertIsInstance(m.d2.transformation_block, _BlockData) - self.assertIsInstance(m.d1.d3.transformation_block, _BlockData) - self.assertIsInstance(m.d1.d4.transformation_block, _BlockData) + self.assertIsInstance(m.d1.transformation_block, BlockData) + self.assertIsInstance(m.d2.transformation_block, BlockData) + self.assertIsInstance(m.d1.d3.transformation_block, BlockData) + self.assertIsInstance(m.d1.d4.transformation_block, BlockData) def check_transformation_blocks_nestedDisjunctions(self, m, transformation): diff --git a/pyomo/gdp/transformed_disjunct.py b/pyomo/gdp/transformed_disjunct.py index 6cf60abf414..287d5ed1652 100644 --- a/pyomo/gdp/transformed_disjunct.py +++ b/pyomo/gdp/transformed_disjunct.py @@ -10,11 +10,11 @@ # ___________________________________________________________________________ from pyomo.common.autoslots import AutoSlots -from pyomo.core.base.block import _BlockData, IndexedBlock +from pyomo.core.base.block import BlockData, IndexedBlock from pyomo.core.base.global_set import UnindexedComponent_index, UnindexedComponent_set -class _TransformedDisjunctData(_BlockData): +class _TransformedDisjunctData(BlockData): __slots__ = ('_src_disjunct',) __autoslot_mappers__ = {'_src_disjunct': AutoSlots.weakref_mapper} @@ -23,7 +23,7 @@ def src_disjunct(self): return None if self._src_disjunct is None else self._src_disjunct() def __init__(self, component): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) # pointer to the Disjunct whose transformation block this is. self._src_disjunct = None diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index fe11975954d..55d273938c5 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -22,7 +22,7 @@ LogicalConstraint, value, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentMap, ComponentSet, OrderedSet from pyomo.opt import TerminationCondition, SolverStatus @@ -330,7 +330,7 @@ def get_gdp_tree(targets, instance, knownBlocks=None): "Target '%s' is not a component on instance " "'%s'!" % (t.name, instance.name) ) - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): _blocks = t.values() if t.is_indexed() else (t,) for block in _blocks: if not block.active: @@ -387,7 +387,7 @@ def is_child_of(parent, child, knownBlocks=None): if knownBlocks is None: knownBlocks = {} tmp = set() - node = child if isinstance(child, (Block, _BlockData)) else child.parent_block() + node = child if isinstance(child, (Block, BlockData)) else child.parent_block() while True: known = knownBlocks.get(node) if known: @@ -452,7 +452,7 @@ def get_src_disjunct(transBlock): Parameters ---------- - transBlock: _BlockData which is in the relaxedDisjuncts IndexedBlock + transBlock: BlockData which is in the relaxedDisjuncts IndexedBlock on a transformation block. """ if ( diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index 79f76a9fc34..3982c7a87ba 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -19,7 +19,7 @@ from pyomo.core import Constraint, Var, Block, Set from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.disable_methods import disable_methods from pyomo.core.base.initializer import ( Initializer, @@ -43,7 +43,7 @@ def complements(a, b): return ComplementarityTuple(a, b) -class _ComplementarityData(_BlockData): +class _ComplementarityData(BlockData): def _canonical_expression(self, e): # Note: as the complimentarity component maintains references to # the original expression (e), it is NOT safe or valid to bypass diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index f1f9d653a8a..c0698165603 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -536,15 +536,15 @@ def solve(self, *args, **kwds): # If the inputs are models, then validate that they have been # constructed! Collect suffix names to try and import from solution. # - from pyomo.core.base.block import _BlockData + from pyomo.core.base.block import BlockData import pyomo.core.base.suffix from pyomo.core.kernel.block import IBlock import pyomo.core.kernel.suffix _model = None for arg in args: - if isinstance(arg, (_BlockData, IBlock)): - if isinstance(arg, _BlockData): + if isinstance(arg, (BlockData, IBlock)): + if isinstance(arg, BlockData): if not arg.is_constructed(): raise RuntimeError( "Attempting to solve model=%s with unconstructed " @@ -553,7 +553,7 @@ def solve(self, *args, **kwds): _model = arg # import suffixes must be on the top-level model - if isinstance(arg, _BlockData): + if isinstance(arg, BlockData): model_suffixes = list( name for ( diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 49cca32eaf9..b4a21a2108f 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -486,7 +486,7 @@ def categorize_valid_components( Parameters ---------- - model: _BlockData + model: BlockData The model tree to walk active: True or None @@ -507,7 +507,7 @@ def categorize_valid_components( Returns ------- - component_map: Dict[type, List[_BlockData]] + component_map: Dict[type, List[BlockData]] A dict mapping component type to a list of block data objects that contain declared component of that type. diff --git a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py index c131b8ad10a..de38a0372d0 100644 --- a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.core.base.PyomoModel import Model -from pyomo.core.base.block import Block, _BlockData +from pyomo.core.base.block import Block, BlockData from pyomo.core.kernel.block import IBlock from pyomo.opt.base.solvers import OptSolver from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler @@ -177,7 +177,7 @@ def _postsolve(self): """ This method should be implemented by subclasses.""" def _set_instance(self, model, kwds={}): - if not isinstance(model, (Model, IBlock, Block, _BlockData)): + if not isinstance(model, (Model, IBlock, Block, BlockData)): msg = ( "The problem instance supplied to the {0} plugin " "'_presolve' method must be a Model or a Block".format(type(self)) diff --git a/pyomo/solvers/plugins/solvers/direct_solver.py b/pyomo/solvers/plugins/solvers/direct_solver.py index 3eab658391c..609a81b2018 100644 --- a/pyomo/solvers/plugins/solvers/direct_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_solver.py @@ -15,7 +15,7 @@ from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( DirectOrPersistentSolver, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.kernel.block import IBlock from pyomo.core.base.suffix import active_import_suffix_generator from pyomo.core.kernel.suffix import import_suffix_generator @@ -79,8 +79,8 @@ def solve(self, *args, **kwds): # _model = None for arg in args: - if isinstance(arg, (_BlockData, IBlock)): - if isinstance(arg, _BlockData): + if isinstance(arg, (BlockData, IBlock)): + if isinstance(arg, BlockData): if not arg.is_constructed(): raise RuntimeError( "Attempting to solve model=%s with unconstructed " @@ -89,7 +89,7 @@ def solve(self, *args, **kwds): _model = arg # import suffixes must be on the top-level model - if isinstance(arg, _BlockData): + if isinstance(arg, BlockData): model_suffixes = list( name for (name, comp) in active_import_suffix_generator(arg) ) diff --git a/pyomo/solvers/plugins/solvers/mosek_direct.py b/pyomo/solvers/plugins/solvers/mosek_direct.py index 5000a2f35c4..5c07c73b94e 100644 --- a/pyomo/solvers/plugins/solvers/mosek_direct.py +++ b/pyomo/solvers/plugins/solvers/mosek_direct.py @@ -558,7 +558,7 @@ def _add_block(self, block): Parameters ---------- - block: Block (scalar Block or single _BlockData) + block: Block (scalar Block or single BlockData) """ var_seq = tuple( block.component_data_objects( diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index 29aa3f2bbf5..d69c050291b 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -12,7 +12,7 @@ from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( DirectOrPersistentSolver, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.kernel.block import IBlock from pyomo.core.base.suffix import active_import_suffix_generator from pyomo.core.kernel.suffix import import_suffix_generator @@ -96,7 +96,7 @@ def add_block(self, block): Parameters ---------- - block: Block (scalar Block or single _BlockData) + block: Block (scalar Block or single BlockData) """ if self._pyomo_model is None: @@ -295,7 +295,7 @@ def remove_block(self, block): Parameters ---------- - block: Block (scalar Block or a single _BlockData) + block: Block (scalar Block or a single BlockData) """ # see PR #366 for discussion about handling indexed @@ -455,7 +455,7 @@ def solve(self, *args, **kwds): self.available(exception_flag=True) # Collect suffix names to try and import from solution. - if isinstance(self._pyomo_model, _BlockData): + if isinstance(self._pyomo_model, BlockData): model_suffixes = list( name for (name, comp) in active_import_suffix_generator(self._pyomo_model) diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 201319ea92a..5ae28baa715 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -11,7 +11,7 @@ import pyomo.environ as pyo import math -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet from pyomo.core.base.var import _GeneralVarData from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr @@ -42,7 +42,7 @@ def _print_var_set(var_set): return s -def _check_var_bounds(m: _BlockData, too_large: float): +def _check_var_bounds(m: BlockData, too_large: float): vars_without_bounds = ComponentSet() vars_with_large_bounds = ComponentSet() for v in m.component_data_objects(pyo.Var, descend_into=True): @@ -90,7 +90,7 @@ def _check_coefficients( def report_scaling( - m: _BlockData, too_large: float = 5e4, too_small: float = 1e-6 + m: BlockData, too_large: float = 5e4, too_small: float = 1e-6 ) -> bool: """ This function logs potentially poorly scaled parts of the model. @@ -107,7 +107,7 @@ def report_scaling( Parameters ---------- - m: _BlockData + m: BlockData The pyomo model or block too_large: float Values above too_large will generate a log entry diff --git a/pyomo/util/slices.py b/pyomo/util/slices.py index 53f6d364219..d85aa3fa926 100644 --- a/pyomo/util/slices.py +++ b/pyomo/util/slices.py @@ -98,7 +98,7 @@ def slice_component_along_sets(comp, sets, context=None): sets: `pyomo.common.collections.ComponentSet` Contains the sets to replace with slices context: `pyomo.core.base.block.Block` or - `pyomo.core.base.block._BlockData` + `pyomo.core.base.block.BlockData` Block below which to search for sets Returns: From c59f91bb1f0d6b94b9e9fb7bbce05ecb6c033c4c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:32:28 -0600 Subject: [PATCH 0729/1178] Renamed _BooleanVarData -> BooleanVarData --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/boolean_var.py | 13 +++++++++---- pyomo/core/base/component.py | 2 +- pyomo/core/plugins/transform/logical_to_linear.py | 4 ++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 4bbd0c9dc44..98eceb45490 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -60,7 +60,7 @@ from pyomo.core.base.var import Var, _VarData, _GeneralVarData, ScalarVar, VarList from pyomo.core.base.boolean_var import ( BooleanVar, - _BooleanVarData, + BooleanVarData, _GeneralBooleanVarData, BooleanVarList, ScalarBooleanVar, diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 246dcea6214..bf9d6159754 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -68,7 +68,7 @@ def __setstate__(self, state): self._boolvar = weakref_ref(state) -class _BooleanVarData(ComponentData, BooleanValue): +class BooleanVarData(ComponentData, BooleanValue): """ This class defines the data for a single variable. @@ -177,6 +177,11 @@ def free(self): return self.unfix() +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData + __renamed__version__ = '6.7.2.dev0' + + def _associated_binary_mapper(encode, val): if val is None: return None @@ -189,7 +194,7 @@ def _associated_binary_mapper(encode, val): return val -class _GeneralBooleanVarData(_BooleanVarData): +class _GeneralBooleanVarData(BooleanVarData): """ This class defines the data for a single Boolean variable. @@ -222,7 +227,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _BooleanVarData + # - BooleanVarData # - ComponentData # - BooleanValue self._component = weakref_ref(component) if (component is not None) else None @@ -390,7 +395,7 @@ def construct(self, data=None): _set.construct() # - # Construct _BooleanVarData objects for all index values + # Construct BooleanVarData objects for all index values # if not self.is_indexed(): self._data[None] = self diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index c91167379fd..0618d6d9d56 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -802,7 +802,7 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: _BooleanVarData, _ConnectorData, _ConstraintData, + # classes: BooleanVarData, _ConnectorData, _ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index 69328032004..da69ca113bd 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -285,7 +285,7 @@ class CnfToLinearVisitor(StreamBasedExpressionVisitor): """Convert CNF logical constraint to linear constraints. Expected expression node types: AndExpression, OrExpression, NotExpression, - AtLeastExpression, AtMostExpression, ExactlyExpression, _BooleanVarData + AtLeastExpression, AtMostExpression, ExactlyExpression, BooleanVarData """ @@ -372,7 +372,7 @@ def beforeChild(self, node, child, child_idx): if child.is_expression_type(): return True, None - # Only thing left should be _BooleanVarData + # Only thing left should be BooleanVarData # # TODO: After the expr_multiple_dispatch is merged, this should # be switched to using as_numeric. From 0c72d9faa267b4f02d3d8b11daad3581701fbb99 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:32:28 -0600 Subject: [PATCH 0730/1178] Renamed _ComplementarityData -> ComplementarityData --- pyomo/mpec/complementarity.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index 3982c7a87ba..aa8db922145 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -43,7 +43,7 @@ def complements(a, b): return ComplementarityTuple(a, b) -class _ComplementarityData(BlockData): +class ComplementarityData(BlockData): def _canonical_expression(self, e): # Note: as the complimentarity component maintains references to # the original expression (e), it is NOT safe or valid to bypass @@ -179,9 +179,14 @@ def set_value(self, cc): ) +class _ComplementarityData(metaclass=RenamedClass): + __renamed__new_class__ = ComplementarityData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Complementarity conditions.") class Complementarity(Block): - _ComponentDataClass = _ComplementarityData + _ComponentDataClass = ComplementarityData def __new__(cls, *args, **kwds): if cls != Complementarity: @@ -298,9 +303,9 @@ def _conditional_block_printer(ostream, idx, data): ) -class ScalarComplementarity(_ComplementarityData, Complementarity): +class ScalarComplementarity(ComplementarityData, Complementarity): def __init__(self, *args, **kwds): - _ComplementarityData.__init__(self, self) + ComplementarityData.__init__(self, self) Complementarity.__init__(self, *args, **kwds) self._data[None] = self self._index = UnindexedComponent_index From e3fe3162f7b314ecdc84834b727fad6148bfa0ba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:32:59 -0600 Subject: [PATCH 0731/1178] Renamed _ConnectorData -> ConnectorData --- pyomo/core/base/component.py | 2 +- pyomo/core/base/connector.py | 15 ++++++++++----- pyomo/core/plugins/transform/expand_connectors.py | 4 ++-- pyomo/repn/standard_repn.py | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 0618d6d9d56..244d9b6a8f5 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -802,7 +802,7 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: BooleanVarData, _ConnectorData, _ConstraintData, + # classes: BooleanVarData, ConnectorData, _ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index 435a2c2fccb..e383b52fc11 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _ConnectorData(ComponentData, NumericValue): +class ConnectorData(ComponentData, NumericValue): """Holds the actual connector information""" __slots__ = ('vars', 'aggregators') @@ -105,6 +105,11 @@ def _iter_vars(self): yield v +class _ConnectorData(metaclass=RenamedClass): + __renamed__new_class__ = ConnectorData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A bundle of variables that can be manipulated together." ) @@ -157,7 +162,7 @@ def __init__(self, *args, **kwd): # IndexedComponent # def _getitem_when_not_present(self, idx): - _conval = self._data[idx] = _ConnectorData(component=self) + _conval = self._data[idx] = ConnectorData(component=self) return _conval def construct(self, data=None): @@ -170,7 +175,7 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True # - # Construct _ConnectorData objects for all index values + # Construct ConnectorData objects for all index values # if self.is_indexed(): self._initialize_members(self._index_set) @@ -258,9 +263,9 @@ def _line_generator(k, v): ) -class ScalarConnector(Connector, _ConnectorData): +class ScalarConnector(Connector, ConnectorData): def __init__(self, *args, **kwd): - _ConnectorData.__init__(self, component=self) + ConnectorData.__init__(self, component=self) Connector.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/core/plugins/transform/expand_connectors.py b/pyomo/core/plugins/transform/expand_connectors.py index 8c02f3e5698..82ec546e593 100644 --- a/pyomo/core/plugins/transform/expand_connectors.py +++ b/pyomo/core/plugins/transform/expand_connectors.py @@ -25,7 +25,7 @@ Var, SortComponents, ) -from pyomo.core.base.connector import _ConnectorData, ScalarConnector +from pyomo.core.base.connector import ConnectorData, ScalarConnector @TransformationFactory.register( @@ -69,7 +69,7 @@ def _apply_to(self, instance, **kwds): # The set of connectors found in the current constraint found = ComponentSet() - connector_types = set([ScalarConnector, _ConnectorData]) + connector_types = set([ScalarConnector, ConnectorData]) for constraint in instance.component_data_objects( Constraint, sort=SortComponents.deterministic ): diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 8600a8a50f6..455e7bd9444 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -1136,7 +1136,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra EXPR.RangedExpression: _collect_comparison, EXPR.EqualityExpression: _collect_comparison, EXPR.ExternalFunctionExpression: _collect_external_fn, - # _ConnectorData : _collect_linear_connector, + # ConnectorData : _collect_linear_connector, # ScalarConnector : _collect_linear_connector, _ParamData: _collect_const, ScalarParam: _collect_const, @@ -1536,7 +1536,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): #EXPR.EqualityExpression : _linear_collect_comparison, #EXPR.ExternalFunctionExpression : _linear_collect_external_fn, ##EXPR.LinearSumExpression : _collect_linear_sum, - ##_ConnectorData : _collect_linear_connector, + ##ConnectorData : _collect_linear_connector, ##ScalarConnector : _collect_linear_connector, ##param._ParamData : _collect_linear_const, ##param.ScalarParam : _collect_linear_const, From d4b72d2b56193ef68589913d9abd7135242609ba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:36:53 -0600 Subject: [PATCH 0732/1178] Renamed _ConstraintData -> ConstraintData --- pyomo/contrib/viewer/report.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/constraint.py | 15 ++++++++++----- pyomo/core/base/logical_constraint.py | 2 +- pyomo/core/base/matrix_constraint.py | 8 ++++---- pyomo/core/beta/dict_objects.py | 4 ++-- pyomo/core/beta/list_objects.py | 4 ++-- pyomo/core/plugins/transform/add_slack_vars.py | 6 +++--- .../core/plugins/transform/equality_transform.py | 4 ++-- pyomo/core/plugins/transform/model.py | 4 ++-- pyomo/core/plugins/transform/scaling.py | 4 ++-- pyomo/core/tests/unit/test_con.py | 2 +- pyomo/gdp/disjunct.py | 2 +- pyomo/gdp/plugins/hull.py | 6 +++--- pyomo/gdp/tests/test_bigm.py | 8 ++++---- pyomo/gdp/util.py | 4 ++-- pyomo/repn/beta/matrix.py | 14 +++++++------- pyomo/repn/plugins/nl_writer.py | 6 +++--- pyomo/repn/plugins/standard_form.py | 4 ++-- pyomo/solvers/plugins/solvers/mosek_persistent.py | 8 ++++---- .../solvers/plugins/solvers/persistent_solver.py | 6 +++--- .../solvers/tests/checks/test_CPLEXPersistent.py | 2 +- .../tests/checks/test_gurobi_persistent.py | 2 +- .../tests/checks/test_xpress_persistent.py | 2 +- pyomo/util/calc_var_value.py | 6 +++--- 26 files changed, 67 insertions(+), 62 deletions(-) diff --git a/pyomo/contrib/viewer/report.py b/pyomo/contrib/viewer/report.py index a1f893bba31..a28e0082212 100644 --- a/pyomo/contrib/viewer/report.py +++ b/pyomo/contrib/viewer/report.py @@ -50,7 +50,7 @@ def get_residual(ui_data, c): values of the constraint body. This function uses the cached values and will not trigger recalculation. If variable values have changed, this may not yield accurate results. - c(_ConstraintData): a constraint or constraint data + c(ConstraintData): a constraint or constraint data Returns: (float) residual """ diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 98eceb45490..9a5337ac2c8 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -70,7 +70,7 @@ simple_constraintlist_rule, ConstraintList, Constraint, - _ConstraintData, + ConstraintData, ) from pyomo.core.base.logical_constraint import ( LogicalConstraint, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 244d9b6a8f5..7d6fc903632 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -802,7 +802,7 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: BooleanVarData, ConnectorData, _ConstraintData, + # classes: BooleanVarData, ConnectorData, ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index fde1160e563..cbae828a459 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -130,7 +130,7 @@ def C_rule(model, i, j): # -class _ConstraintData(ActiveComponentData): +class ConstraintData(ActiveComponentData): """ This class defines the data for a single constraint. @@ -165,7 +165,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -280,7 +280,12 @@ def get_value(self): raise NotImplementedError -class _GeneralConstraintData(_ConstraintData): +class _ConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = ConstraintData + __renamed__version__ = '6.7.2.dev0' + + +class _GeneralConstraintData(ConstraintData): """ This class defines the data for a single general constraint. @@ -312,7 +317,7 @@ def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -897,7 +902,7 @@ def __init__(self, *args, **kwds): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave - # like _ConstraintData objects where set_value does not handle + # like ConstraintData objects where set_value does not handle # Constraint.Skip but expects a valid expression or None. # @property diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index f32d727931a..3a7bca75960 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -373,7 +373,7 @@ def display(self, prefix="", ostream=None): # # Checks flags like Constraint.Skip, etc. before actually creating a - # constraint object. Returns the _ConstraintData object when it should be + # constraint object. Returns the ConstraintData object when it should be # added to the _data dict; otherwise, None is returned or an exception # is raised. # diff --git a/pyomo/core/base/matrix_constraint.py b/pyomo/core/base/matrix_constraint.py index adc9742302e..8dac7c3d24b 100644 --- a/pyomo/core/base/matrix_constraint.py +++ b/pyomo/core/base/matrix_constraint.py @@ -19,7 +19,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.numeric_expr import LinearExpression from pyomo.core.base.component import ModelComponentFactory -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.repn.standard_repn import StandardRepn from collections.abc import Mapping @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _MatrixConstraintData(_ConstraintData): +class _MatrixConstraintData(ConstraintData): """ This class defines the data for a single linear constraint derived from a canonical form Ax=b constraint. @@ -104,7 +104,7 @@ def __init__(self, index, component_ref): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = component_ref @@ -209,7 +209,7 @@ def index(self): return self._index # - # Abstract Interface (_ConstraintData) + # Abstract Interface (ConstraintData) # @property diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index a8298b08e63..53d39939db2 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -15,7 +15,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData from pyomo.core.base.expression import IndexedExpression, _ExpressionData @@ -193,7 +193,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ConstraintData, *args, **kwds) + ComponentDict.__init__(self, ConstraintData, *args, **kwds) class ObjectiveDict(ComponentDict, IndexedObjective): diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index f53997fed17..e8b40e6da53 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -15,7 +15,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData from pyomo.core.base.expression import IndexedExpression, _ExpressionData @@ -241,7 +241,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ConstraintData, *args, **kwds) + ComponentList.__init__(self, ConstraintData, *args, **kwds) class XObjectiveList(ComponentList, IndexedObjective): diff --git a/pyomo/core/plugins/transform/add_slack_vars.py b/pyomo/core/plugins/transform/add_slack_vars.py index 6b5096d315c..0007f8de7ad 100644 --- a/pyomo/core/plugins/transform/add_slack_vars.py +++ b/pyomo/core/plugins/transform/add_slack_vars.py @@ -23,7 +23,7 @@ from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base import ComponentUID -from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.common.deprecation import deprecation_warning @@ -42,7 +42,7 @@ def target_list(x): # [ESJ 07/15/2020] We have to just pass it through because we need the # instance in order to be able to do anything about it... return [x] - elif isinstance(x, (Constraint, _ConstraintData)): + elif isinstance(x, (Constraint, ConstraintData)): return [x] elif hasattr(x, '__iter__'): ans = [] @@ -53,7 +53,7 @@ def target_list(x): deprecation_msg = None # same as above... ans.append(i) - elif isinstance(i, (Constraint, _ConstraintData)): + elif isinstance(i, (Constraint, ConstraintData)): ans.append(i) else: raise ValueError( diff --git a/pyomo/core/plugins/transform/equality_transform.py b/pyomo/core/plugins/transform/equality_transform.py index a1a1b72f146..99291c2227c 100644 --- a/pyomo/core/plugins/transform/equality_transform.py +++ b/pyomo/core/plugins/transform/equality_transform.py @@ -66,7 +66,7 @@ def _create_using(self, model, **kwds): con = equality.__getattribute__(con_name) # - # Get all _ConstraintData objects + # Get all ConstraintData objects # # We need to get the keys ahead of time because we are modifying # con._data on-the-fly. @@ -104,7 +104,7 @@ def _create_using(self, model, **kwds): con.add(ub_name, new_expr) # Since we explicitly `continue` for equality constraints, we - # can safely remove the old _ConstraintData object + # can safely remove the old ConstraintData object del con._data[ndx] return equality.create() diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index db8376afd29..7ee268a4292 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -55,8 +55,8 @@ def to_standard_form(self): # N.B. Structure hierarchy: # # active_components: {class: {attr_name: object}} - # object -> Constraint: ._data: {ndx: _ConstraintData} - # _ConstraintData: .lower, .body, .upper + # object -> Constraint: ._data: {ndx: ConstraintData} + # ConstraintData: .lower, .body, .upper # # So, altogether, we access a lower bound via # diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index ad894b31fde..6b83a2378d1 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -15,7 +15,7 @@ Var, Constraint, Objective, - _ConstraintData, + ConstraintData, _ObjectiveData, Suffix, value, @@ -197,7 +197,7 @@ def _apply_to(self, model, rename=True): already_scaled.add(id(c)) # perform the constraint/objective scaling and variable sub scaling_factor = component_scaling_factor_map[c] - if isinstance(c, _ConstraintData): + if isinstance(c, ConstraintData): body = scaling_factor * replace_expressions( expr=c.body, substitution_map=variable_substitution_dict, diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index 6ed19c1bcfd..2fa6c24de9c 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -1388,7 +1388,7 @@ def test_empty_singleton(self): # Even though we construct a ScalarConstraint, # if it is not initialized that means it is "empty" # and we should encounter errors when trying to access the - # _ConstraintData interface methods until we assign + # ConstraintData interface methods until we assign # something to the constraint. # self.assertEqual(a._constructed, True) diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index e6d8d709425..de021d37547 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -542,7 +542,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 5b9d2ad08a9..134a3d16d66 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -750,20 +750,20 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) - # map the _ConstraintDatas (we mapped the container above) + # map the ConstraintDatas (we mapped the container above) constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'eq'] ) constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) - # map to the _ConstraintData (And yes, for + # map to the ConstraintData (And yes, for # ScalarConstraints, this is overwriting the map to the # container we made above, and that is what I want to # happen. ScalarConstraints will map to lists. For # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the - # _ConstraintDatas to each other above) + # ConstraintDatas to each other above) constraint_map.transformed_constraints[c].append( newConstraint[name, 'eq'] ) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index c6ac49f6d36..bf0239d15e0 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -27,7 +27,7 @@ value, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error -from pyomo.core.base import constraint, _ConstraintData +from pyomo.core.base import constraint, ConstraintData from pyomo.core.expr.compare import ( assertExpressionsEqual, assertExpressionsStructurallyEqual, @@ -653,14 +653,14 @@ def test_disjunct_and_constraint_maps(self): if src[0]: # equality self.assertEqual(len(transformed), 2) - self.assertIsInstance(transformed[0], _ConstraintData) - self.assertIsInstance(transformed[1], _ConstraintData) + self.assertIsInstance(transformed[0], ConstraintData) + self.assertIsInstance(transformed[1], ConstraintData) self.assertIs(bigm.get_src_constraint(transformed[0]), srcDisjunct.c) self.assertIs(bigm.get_src_constraint(transformed[1]), srcDisjunct.c) else: # >= self.assertEqual(len(transformed), 1) - self.assertIsInstance(transformed[0], _ConstraintData) + self.assertIsInstance(transformed[0], ConstraintData) # check reverse map from the container self.assertIs(bigm.get_src_constraint(transformed[0]), srcDisjunct.c) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 55d273938c5..2164671ea16 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -525,13 +525,13 @@ def get_transformed_constraints(srcConstraint): Parameters ---------- - srcConstraint: ScalarConstraint or _ConstraintData, which must be in + srcConstraint: ScalarConstraint or ConstraintData, which must be in the subtree of a transformed Disjunct """ if srcConstraint.is_indexed(): raise GDP_Error( "Argument to get_transformed_constraint should be " - "a ScalarConstraint or _ConstraintData. (If you " + "a ScalarConstraint or ConstraintData. (If you " "want the container for all transformed constraints " "from an IndexedDisjunction, this is the parent " "component of a transformed constraint originating " diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index 916b0daf755..0201c46eb18 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -24,7 +24,7 @@ Constraint, IndexedConstraint, ScalarConstraint, - _ConstraintData, + ConstraintData, ) from pyomo.core.expr.numvalue import native_numeric_types from pyomo.repn import generate_standard_repn @@ -247,7 +247,7 @@ def _get_bound(exp): constraint_containers_removed += 1 for constraint, index in constraint_data_to_remove: # Note that this del is not needed: assigning Constraint.Skip - # above removes the _ConstraintData from the _data dict. + # above removes the ConstraintData from the _data dict. # del constraint[index] constraints_removed += 1 for block, constraint in constraint_containers_to_remove: @@ -348,12 +348,12 @@ def _get_bound(exp): ) -# class _LinearConstraintData(_ConstraintData,LinearCanonicalRepn): +# class _LinearConstraintData(ConstraintData,LinearCanonicalRepn): # # This change breaks this class, but it's unclear whether this # is being used... # -class _LinearConstraintData(_ConstraintData): +class _LinearConstraintData(ConstraintData): """ This class defines the data for a single linear constraint in canonical form. @@ -393,7 +393,7 @@ def __init__(self, index, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -442,7 +442,7 @@ def __init__(self, index, component=None): # These lines represent in-lining of the # following constructors: # - _LinearConstraintData - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -584,7 +584,7 @@ def constant(self): return sum(terms) # - # Abstract Interface (_ConstraintData) + # Abstract Interface (ConstraintData) # @property diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ee5b65149ae..29d841248da 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -69,7 +69,7 @@ minimize, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData from pyomo.core.base.objective import ( ScalarObjective, @@ -134,7 +134,7 @@ class NLWriterInfo(object): The list of (unfixed) Pyomo model variables in the order written to the NL file - constraints: List[_ConstraintData] + constraints: List[ConstraintData] The list of (active) Pyomo model constraints in the order written to the NL file @@ -466,7 +466,7 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (_VarData, _ConstraintData, _ObjectiveData)): + elif isinstance(obj, (_VarData, ConstraintData, _ObjectiveData)): missing_component_data.add(obj) elif isinstance(obj, (Var, Constraint, Objective)): # Expand this indexed component to store the diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 239cd845930..e6dc217acc9 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -76,11 +76,11 @@ class LinearStandardFormInfo(object): The constraint right-hand sides. - rows : List[Tuple[_ConstraintData, int]] + rows : List[Tuple[ConstraintData, int]] The list of Pyomo constraint objects corresponding to the rows in `A`. Each element in the list is a 2-tuple of - (_ConstraintData, row_multiplier). The `row_multiplier` will be + (ConstraintData, row_multiplier). The `row_multiplier` will be +/- 1 indicating if the row was multiplied by -1 (corresponding to a constraint lower bound) or +1 (upper bound). diff --git a/pyomo/solvers/plugins/solvers/mosek_persistent.py b/pyomo/solvers/plugins/solvers/mosek_persistent.py index 97f88e0cb9a..9e7f8de1b41 100644 --- a/pyomo/solvers/plugins/solvers/mosek_persistent.py +++ b/pyomo/solvers/plugins/solvers/mosek_persistent.py @@ -85,7 +85,7 @@ def add_constraints(self, con_seq): Parameters ---------- - con_seq: tuple/list of Constraint (scalar Constraint or single _ConstraintData) + con_seq: tuple/list of Constraint (scalar Constraint or single ConstraintData) """ self._add_constraints(con_seq) @@ -137,7 +137,7 @@ def remove_constraint(self, solver_con): To remove a conic-domain, you should use the remove_block method. Parameters ---------- - solver_con: Constraint (scalar Constraint or single _ConstraintData) + solver_con: Constraint (scalar Constraint or single ConstraintData) """ self.remove_constraints(solver_con) @@ -151,7 +151,7 @@ def remove_constraints(self, *solver_cons): Parameters ---------- - *solver_cons: Constraint (scalar Constraint or single _ConstraintData) + *solver_cons: Constraint (scalar Constraint or single ConstraintData) """ lq_cons = tuple( itertools.filterfalse(lambda x: isinstance(x, _ConicBase), solver_cons) @@ -205,7 +205,7 @@ def update_vars(self, *solver_vars): changing variable types and bounds. Parameters ---------- - *solver_var: Constraint (scalar Constraint or single _ConstraintData) + *solver_var: Constraint (scalar Constraint or single ConstraintData) """ try: var_ids = [] diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index d69c050291b..79cd669dd71 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -132,7 +132,7 @@ def add_constraint(self, con): Parameters ---------- - con: Constraint (scalar Constraint or single _ConstraintData) + con: Constraint (scalar Constraint or single ConstraintData) """ if self._pyomo_model is None: @@ -208,7 +208,7 @@ def add_column(self, model, var, obj_coef, constraints, coefficients): model: pyomo ConcreteModel to which the column will be added var: Var (scalar Var or single _VarData) obj_coef: float, pyo.Param - constraints: list of scalar Constraints of single _ConstraintDatas + constraints: list of scalar Constraints of single ConstraintDatas coefficients: list of the coefficient to put on var in the associated constraint """ @@ -328,7 +328,7 @@ def remove_constraint(self, con): Parameters ---------- - con: Constraint (scalar Constraint or single _ConstraintData) + con: Constraint (scalar Constraint or single ConstraintData) """ # see PR #366 for discussion about handling indexed diff --git a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py index 91a60eee9dd..442212d4fbb 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py +++ b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py @@ -101,7 +101,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/checks/test_gurobi_persistent.py b/pyomo/solvers/tests/checks/test_gurobi_persistent.py index a2c089207e5..812390c23a4 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_persistent.py +++ b/pyomo/solvers/tests/checks/test_gurobi_persistent.py @@ -382,7 +382,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/checks/test_xpress_persistent.py b/pyomo/solvers/tests/checks/test_xpress_persistent.py index ddae860cd92..dcd36780f62 100644 --- a/pyomo/solvers/tests/checks/test_xpress_persistent.py +++ b/pyomo/solvers/tests/checks/test_xpress_persistent.py @@ -262,7 +262,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index b5e620fea07..d5bceb5c67b 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -12,7 +12,7 @@ from pyomo.common.errors import IterationLimitError from pyomo.common.numeric_types import native_numeric_types, native_complex_types, value from pyomo.core.expr.calculus.derivatives import differentiate -from pyomo.core.base.constraint import Constraint, _ConstraintData +from pyomo.core.base.constraint import Constraint, ConstraintData import logging @@ -55,7 +55,7 @@ def calculate_variable_from_constraint( ----------- variable: :py:class:`_VarData` The variable to solve for - constraint: :py:class:`_ConstraintData` or relational expression or `tuple` + constraint: :py:class:`ConstraintData` or relational expression or `tuple` The equality constraint to use to solve for the variable value. May be a `ConstraintData` object or any valid argument for ``Constraint(expr=<>)`` (i.e., a relational expression or 2- or @@ -81,7 +81,7 @@ def calculate_variable_from_constraint( """ # Leverage all the Constraint logic to process the incoming tuple/expression - if not isinstance(constraint, _ConstraintData): + if not isinstance(constraint, ConstraintData): constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() From 61c91d065eaf51cbcef2636a836566777239edc0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:37:15 -0600 Subject: [PATCH 0733/1178] Renamed _DisjunctData -> DisjunctData --- pyomo/contrib/gdp_bounds/info.py | 4 ++-- pyomo/gdp/disjunct.py | 27 ++++++++++++++++----------- pyomo/gdp/plugins/hull.py | 2 +- pyomo/gdp/tests/test_bigm.py | 2 +- pyomo/gdp/util.py | 4 ++-- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index 6f39af5908d..db3f6d0846d 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -37,8 +37,8 @@ def disjunctive_bound(var, scope): Args: var (_VarData): Variable for which to compute bound scope (Component): The scope in which to compute the bound. If not a - _DisjunctData, it will walk up the tree and use the scope of the - most immediate enclosing _DisjunctData. + DisjunctData, it will walk up the tree and use the scope of the + most immediate enclosing DisjunctData. Returns: numeric: the tighter of either the disjunctive lower bound, the diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index de021d37547..dd9d2b4638c 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -412,7 +412,7 @@ def process(arg): return (_Initializer.deferred_value, arg) -class _DisjunctData(BlockData): +class DisjunctData(BlockData): __autoslot_mappers__ = {'_transformation_block': AutoSlots.weakref_mapper} _Block_reserved_words = set() @@ -434,23 +434,28 @@ def __init__(self, component): self._transformation_block = None def activate(self): - super(_DisjunctData, self).activate() + super(DisjunctData, self).activate() self.indicator_var.unfix() def deactivate(self): - super(_DisjunctData, self).deactivate() + super(DisjunctData, self).deactivate() self.indicator_var.fix(False) def _deactivate_without_fixing_indicator(self): - super(_DisjunctData, self).deactivate() + super(DisjunctData, self).deactivate() def _activate_without_unfixing_indicator(self): - super(_DisjunctData, self).activate() + super(DisjunctData, self).activate() + + +class _DisjunctData(metaclass=RenamedClass): + __renamed__new_class__ = DisjunctData + __renamed__version__ = '6.7.2.dev0' @ModelComponentFactory.register("Disjunctive blocks.") class Disjunct(Block): - _ComponentDataClass = _DisjunctData + _ComponentDataClass = DisjunctData def __new__(cls, *args, **kwds): if cls != Disjunct: @@ -475,7 +480,7 @@ def __init__(self, *args, **kwargs): # def _deactivate_without_fixing_indicator(self): # # Ideally, this would be a super call from this class. However, # # doing that would trigger a call to deactivate() on all the - # # _DisjunctData objects (exactly what we want to avoid!) + # # DisjunctData objects (exactly what we want to avoid!) # # # # For the time being, we will do something bad and directly call # # the base class method from where we would otherwise want to @@ -484,7 +489,7 @@ def __init__(self, *args, **kwargs): def _activate_without_unfixing_indicator(self): # Ideally, this would be a super call from this class. However, # doing that would trigger a call to deactivate() on all the - # _DisjunctData objects (exactly what we want to avoid!) + # DisjunctData objects (exactly what we want to avoid!) # # For the time being, we will do something bad and directly call # the base class method from where we would otherwise want to @@ -495,7 +500,7 @@ def _activate_without_unfixing_indicator(self): component_data._activate_without_unfixing_indicator() -class ScalarDisjunct(_DisjunctData, Disjunct): +class ScalarDisjunct(DisjunctData, Disjunct): def __init__(self, *args, **kwds): ## FIXME: This is a HACK to get around a chicken-and-egg issue ## where BlockData creates the indicator_var *before* @@ -503,7 +508,7 @@ def __init__(self, *args, **kwds): self._defer_construction = True self._suppress_ctypes = set() - _DisjunctData.__init__(self, self) + DisjunctData.__init__(self, self) Disjunct.__init__(self, *args, **kwds) self._data[None] = self self._index = UnindexedComponent_index @@ -524,7 +529,7 @@ def active(self): return any(d.active for d in self._data.values()) -_DisjunctData._Block_reserved_words = set(dir(Disjunct())) +DisjunctData._Block_reserved_words = set(dir(Disjunct())) class _DisjunctionData(ActiveComponentData): diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 134a3d16d66..854366c0cf0 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -42,7 +42,7 @@ Binary, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error -from pyomo.gdp.disjunct import _DisjunctData +from pyomo.gdp.disjunct import DisjunctData from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.gdp.transformed_disjunct import _TransformedDisjunct from pyomo.gdp.util import ( diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index bf0239d15e0..d5dcef3ba58 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -2196,7 +2196,7 @@ def test_do_not_assume_nested_indicators_local(self): class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the - # _DisjunctDatas in an IndexedDisjunction that the xor constraint + # DisjunctDatas in an IndexedDisjunction that the xor constraint # created on the parent block will still be indexed as expected. def test_xor_constraint(self): ct.check_indexed_xor_constraints_with_targets(self, 'bigm') diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 2164671ea16..686253b0179 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.gdp import GDP_Error, Disjunction -from pyomo.gdp.disjunct import _DisjunctData, Disjunct +from pyomo.gdp.disjunct import DisjunctData, Disjunct import pyomo.core.expr as EXPR from pyomo.core.base.component import _ComponentBase @@ -493,7 +493,7 @@ def get_src_constraint(transformedConstraint): def _find_parent_disjunct(constraint): # traverse up until we find the disjunct this constraint lives on parent_disjunct = constraint.parent_block() - while not isinstance(parent_disjunct, _DisjunctData): + while not isinstance(parent_disjunct, DisjunctData): if parent_disjunct is None: raise GDP_Error( "Constraint '%s' is not on a disjunct and so was not " From 47a7e26da00a1520e5903e16baeb6ce076b155c6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:37:32 -0600 Subject: [PATCH 0734/1178] Renamed _DisjunctionData -> DisjunctionData --- pyomo/core/base/component.py | 2 +- pyomo/gdp/disjunct.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 7d6fc903632..dcaf976356b 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, - # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, + # _ParamData,_GeneralVarData, _GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index dd9d2b4638c..658ead27783 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -532,7 +532,7 @@ def active(self): DisjunctData._Block_reserved_words = set(dir(Disjunct())) -class _DisjunctionData(ActiveComponentData): +class DisjunctionData(ActiveComponentData): __slots__ = ('disjuncts', 'xor', '_algebraic_constraint', '_transformation_map') __autoslot_mappers__ = {'_algebraic_constraint': AutoSlots.weakref_mapper} _NoArgument = (0,) @@ -625,9 +625,14 @@ def set_value(self, expr): self.disjuncts.append(disjunct) +class _DisjunctionData(metaclass=RenamedClass): + __renamed__new_class__ = DisjunctionData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Disjunction expressions.") class Disjunction(ActiveIndexedComponent): - _ComponentDataClass = _DisjunctionData + _ComponentDataClass = DisjunctionData def __new__(cls, *args, **kwds): if cls != Disjunction: @@ -768,9 +773,9 @@ def _pprint(self): ) -class ScalarDisjunction(_DisjunctionData, Disjunction): +class ScalarDisjunction(DisjunctionData, Disjunction): def __init__(self, *args, **kwds): - _DisjunctionData.__init__(self, component=self) + DisjunctionData.__init__(self, component=self) Disjunction.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -781,7 +786,7 @@ def __init__(self, *args, **kwds): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave - # like _DisjunctionData objects where set_value does not handle + # like DisjunctionData objects where set_value does not handle # Disjunction.Skip but expects a valid expression or None. # From 4320bc1c068ea2b812f22e149378fe9fc7770291 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:38:42 -0600 Subject: [PATCH 0735/1178] Renamed _ExpressionData -> ExpressionData --- pyomo/contrib/mcpp/pyomo_mcpp.py | 4 ++-- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/expression.py | 11 ++++++++--- pyomo/core/base/objective.py | 4 ++-- pyomo/core/beta/dict_objects.py | 4 ++-- pyomo/core/beta/list_objects.py | 4 ++-- pyomo/gdp/tests/test_util.py | 6 +++--- pyomo/repn/plugins/ampl/ampl_.py | 4 ++-- pyomo/repn/standard_repn.py | 6 +++--- pyomo/repn/util.py | 4 ++-- 10 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/mcpp/pyomo_mcpp.py b/pyomo/contrib/mcpp/pyomo_mcpp.py index 35e883f98da..1375ae61c50 100644 --- a/pyomo/contrib/mcpp/pyomo_mcpp.py +++ b/pyomo/contrib/mcpp/pyomo_mcpp.py @@ -20,7 +20,7 @@ from pyomo.common.fileutils import Library from pyomo.core import value, Expression from pyomo.core.base.block import SubclassOf -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.numvalue import nonpyomo_leaf_types from pyomo.core.expr.numeric_expr import ( AbsExpression, @@ -307,7 +307,7 @@ def exitNode(self, node, data): ans = self.mcpp.newConstant(node) elif not node.is_expression_type(): ans = self.register_num(node) - elif type(node) in SubclassOf(Expression) or isinstance(node, _ExpressionData): + elif type(node) in SubclassOf(Expression) or isinstance(node, ExpressionData): ans = data[0] else: raise RuntimeError("Unhandled expression type: %s" % (type(node))) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 9a5337ac2c8..d875065d502 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -36,7 +36,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base.config import PyomoOptions -from pyomo.core.base.expression import Expression, _ExpressionData +from pyomo.core.base.expression import Expression, ExpressionData from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 3ce998b62a4..e21613fcbb1 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -36,7 +36,7 @@ logger = logging.getLogger('pyomo.core') -class _ExpressionData(numeric_expr.NumericValue): +class ExpressionData(numeric_expr.NumericValue): """ An object that defines a named expression. @@ -137,13 +137,18 @@ def is_fixed(self): """A boolean indicating whether this expression is fixed.""" raise NotImplementedError - # _ExpressionData should never return False because + # ExpressionData should never return False because # they can store subexpressions that contain variables def is_potentially_variable(self): return True -class _GeneralExpressionDataImpl(_ExpressionData): +class _ExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = ExpressionData + __renamed__version__ = '6.7.2.dev0' + + +class _GeneralExpressionDataImpl(ExpressionData): """ An object that defines an expression that is never cloned diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fcc63755f2b..d259358dcd7 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -28,7 +28,7 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import _ExpressionData, _GeneralExpressionDataImpl +from pyomo.core.base.expression import ExpressionData, _GeneralExpressionDataImpl from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, @@ -86,7 +86,7 @@ def O_rule(model, i, j): # -class _ObjectiveData(_ExpressionData): +class _ObjectiveData(ExpressionData): """ This class defines the data for a single objective. diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index 53d39939db2..2b23d81e91a 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -17,7 +17,7 @@ from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData -from pyomo.core.base.expression import IndexedExpression, _ExpressionData +from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableMapping from collections.abc import Mapping @@ -211,4 +211,4 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ExpressionData, *args, **kwds) + ComponentDict.__init__(self, ExpressionData, *args, **kwds) diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index e8b40e6da53..dd199eb70cd 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -17,7 +17,7 @@ from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData -from pyomo.core.base.expression import IndexedExpression, _ExpressionData +from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableSequence @@ -259,4 +259,4 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ExpressionData, *args, **kwds) + ComponentList.__init__(self, ExpressionData, *args, **kwds) diff --git a/pyomo/gdp/tests/test_util.py b/pyomo/gdp/tests/test_util.py index fd555fc2f59..8ea72af37da 100644 --- a/pyomo/gdp/tests/test_util.py +++ b/pyomo/gdp/tests/test_util.py @@ -13,7 +13,7 @@ from pyomo.core import ConcreteModel, Var, Expression, Block, RangeSet, Any import pyomo.core.expr as EXPR -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.gdp.util import ( clone_without_expression_components, is_child_of, @@ -40,7 +40,7 @@ def test_clone_without_expression_components(self): test = clone_without_expression_components(base, {}) self.assertIsNot(base, test) self.assertEqual(base(), test()) - self.assertIsInstance(base, _ExpressionData) + self.assertIsInstance(base, ExpressionData) self.assertIsInstance(test, EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1, test()) @@ -51,7 +51,7 @@ def test_clone_without_expression_components(self): self.assertEqual(base(), test()) self.assertIsInstance(base, EXPR.SumExpression) self.assertIsInstance(test, EXPR.SumExpression) - self.assertIsInstance(base.arg(0), _ExpressionData) + self.assertIsInstance(base.arg(0), ExpressionData) self.assertIsInstance(test.arg(0), EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1 + 3, test()) diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index f422a085a3c..c6357cbecd9 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -33,7 +33,7 @@ from pyomo.core.base import ( SymbolMap, NameLabeler, - _ExpressionData, + ExpressionData, SortComponents, var, param, @@ -724,7 +724,7 @@ def _print_nonlinear_terms_NL(self, exp): self._print_nonlinear_terms_NL(exp.arg(0)) self._print_nonlinear_terms_NL(exp.arg(1)) - elif isinstance(exp, (_ExpressionData, IIdentityExpression)): + elif isinstance(exp, (ExpressionData, IIdentityExpression)): self._print_nonlinear_terms_NL(exp.expr) else: diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 455e7bd9444..70368dd3d7e 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -20,7 +20,7 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective -from pyomo.core.base import _ExpressionData, Expression +from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData @@ -1152,7 +1152,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, - _ExpressionData: _collect_identity, + ExpressionData: _collect_identity, Expression: _collect_identity, _GeneralObjectiveData: _collect_identity, ScalarObjective: _collect_identity, @@ -1551,7 +1551,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, - _ExpressionData : _linear_collect_identity, + ExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, _GeneralObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index b4a21a2108f..7351ea51c58 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -40,7 +40,7 @@ SortComponents, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.numvalue import is_fixed, value import pyomo.core.expr as EXPR import pyomo.core.kernel as kernel @@ -55,7 +55,7 @@ EXPR.NPV_SumExpression, } _named_subexpression_types = ( - _ExpressionData, + ExpressionData, kernel.expression.expression, kernel.objective.objective, ) From 86192304769e54cfcd3900e7878d71a767b5446a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:40:16 -0600 Subject: [PATCH 0736/1178] Renamed _FiniteRangeSetData -> FiniteRangeSetData --- pyomo/core/base/set.py | 17 +++++++++++------ pyomo/core/tests/unit/test_set.py | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b3277ab3260..319f3b0e5ae 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2648,7 +2648,7 @@ def ranges(self): return iter(self._ranges) -class _FiniteRangeSetData( +class FiniteRangeSetData( _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, _InfiniteRangeSetData ): __slots__ = () @@ -2672,7 +2672,7 @@ def _iter_impl(self): # iterate over it nIters = len(self._ranges) - 1 if not nIters: - yield from _FiniteRangeSetData._range_gen(self._ranges[0]) + yield from FiniteRangeSetData._range_gen(self._ranges[0]) return # The trick here is that we need to remove any duplicates from @@ -2683,7 +2683,7 @@ def _iter_impl(self): for r in self._ranges: # Note: there should always be at least 1 member in each # NumericRange - i = _FiniteRangeSetData._range_gen(r) + i = FiniteRangeSetData._range_gen(r) iters.append([next(i), i]) iters.sort(reverse=True, key=lambda x: x[0]) @@ -2756,6 +2756,11 @@ def ord(self, item): domain = _InfiniteRangeSetData.domain +class _FiniteRangeSetData(metaclass=RenamedClass): + __renamed__new_class__ = FiniteRangeSetData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A sequence of numeric values. RangeSet(start,end,step) is a sequence " "starting a value 'start', and increasing in values by 'step' until a " @@ -3120,7 +3125,7 @@ def construct(self, data=None): old_ranges.reverse() while old_ranges: r = old_ranges.pop() - for i, val in enumerate(_FiniteRangeSetData._range_gen(r)): + for i, val in enumerate(FiniteRangeSetData._range_gen(r)): if not _filter(_block, val): split_r = r.range_difference((NumericRange(val, val, 0),)) if len(split_r) == 2: @@ -3233,9 +3238,9 @@ class InfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class FiniteScalarRangeSet(_ScalarOrderedSetMixin, _FiniteRangeSetData, RangeSet): +class FiniteScalarRangeSet(_ScalarOrderedSetMixin, FiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): - _FiniteRangeSetData.__init__(self, component=self) + FiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 4bbac6ecaa0..3639aa82c73 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -60,7 +60,7 @@ FiniteSetOf, InfiniteSetOf, RangeSet, - _FiniteRangeSetData, + FiniteRangeSetData, _InfiniteRangeSetData, FiniteScalarRangeSet, InfiniteScalarRangeSet, @@ -1285,13 +1285,13 @@ def test_is_functions(self): self.assertTrue(i.isdiscrete()) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertIsInstance(i, _FiniteRangeSetData) + self.assertIsInstance(i, FiniteRangeSetData) i = RangeSet(1, 3) self.assertTrue(i.isdiscrete()) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertIsInstance(i, _FiniteRangeSetData) + self.assertIsInstance(i, FiniteRangeSetData) i = RangeSet(1, 3, 0) self.assertFalse(i.isdiscrete()) From 4ea182da5dab8f6994cbf7bc17a0cd1fc1b2f1ec Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:40:42 -0600 Subject: [PATCH 0737/1178] Renamed _FiniteSetData -> FiniteSetData --- pyomo/core/base/set.py | 17 +++++++++++------ pyomo/core/tests/unit/test_set.py | 8 ++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 319f3b0e5ae..8db64620d5c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1294,7 +1294,7 @@ def ranges(self): yield NonNumericRange(i) -class _FiniteSetData(_FiniteSetMixin, _SetData): +class FiniteSetData(_FiniteSetMixin, _SetData): """A general unordered iterable Set""" __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') @@ -1470,6 +1470,11 @@ def pop(self): return self._values.pop() +class _FiniteSetData(metaclass=RenamedClass): + __renamed__new_class__ = FiniteSetData + __renamed__version__ = '6.7.2.dev0' + + class _ScalarOrderedSetMixin(object): # This mixin is required because scalar ordered sets implement # __getitem__() as an alias of at() @@ -1630,7 +1635,7 @@ def _to_0_based_index(self, item): ) -class _OrderedSetData(_OrderedSetMixin, _FiniteSetData): +class _OrderedSetData(_OrderedSetMixin, FiniteSetData): """ This class defines the base class for an ordered set of concrete data. @@ -1652,7 +1657,7 @@ class _OrderedSetData(_OrderedSetMixin, _FiniteSetData): def __init__(self, component): self._values = {} self._ordered_values = [] - _FiniteSetData.__init__(self, component=component) + FiniteSetData.__init__(self, component=component) def _iter_impl(self): """ @@ -2034,7 +2039,7 @@ def __new__(cls, *args, **kwds): elif ordered is Set.SortedOrder: newObj._ComponentDataClass = _SortedSetData else: - newObj._ComponentDataClass = _FiniteSetData + newObj._ComponentDataClass = FiniteSetData return newObj @overload @@ -2388,9 +2393,9 @@ def __getitem__(self, index) -> _SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore -class FiniteScalarSet(_FiniteSetData, Set): +class FiniteScalarSet(FiniteSetData, Set): def __init__(self, **kwds): - _FiniteSetData.__init__(self, component=self) + FiniteSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 3639aa82c73..a9b9fb9469b 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -82,7 +82,7 @@ SetProduct_FiniteSet, SetProduct_OrderedSet, _SetData, - _FiniteSetData, + FiniteSetData, _InsertionOrderSetData, _SortedSetData, _FiniteSetMixin, @@ -4137,9 +4137,9 @@ def test_indexed_set(self): self.assertFalse(m.I[1].isordered()) self.assertFalse(m.I[2].isordered()) self.assertFalse(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _FiniteSetData) - self.assertIs(type(m.I[2]), _FiniteSetData) - self.assertIs(type(m.I[3]), _FiniteSetData) + self.assertIs(type(m.I[1]), FiniteSetData) + self.assertIs(type(m.I[2]), FiniteSetData) + self.assertIs(type(m.I[3]), FiniteSetData) self.assertEqual(m.I.data(), {1: (1,), 2: (2,), 3: (4,)}) # Explicit (constant) construction From 3a86baa8bcb6152edca7c4e687488e6c5de08137 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:41:02 -0600 Subject: [PATCH 0738/1178] Renamed _GeneralBooleanVarData -> GeneralBooleanVarData --- pyomo/contrib/cp/repn/docplex_writer.py | 4 +-- .../logical_to_disjunctive_walker.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/boolean_var.py | 27 +++++++++++-------- pyomo/core/base/component.py | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 8356a1e752f..510bbf4e398 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -60,7 +60,7 @@ ) from pyomo.core.base.boolean_var import ( ScalarBooleanVar, - _GeneralBooleanVarData, + GeneralBooleanVarData, IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData @@ -964,7 +964,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): _GeneralVarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, - _GeneralBooleanVarData: _before_boolean_var, + GeneralBooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, _GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index d5f13e91535..548078f55f8 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -209,7 +209,7 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher = {} _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[BV._GeneralBooleanVarData] = _dispatch_boolean_var +_before_child_dispatcher[BV.GeneralBooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var _before_child_dispatcher[_ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index d875065d502..bb62cb96782 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -61,7 +61,7 @@ from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, - _GeneralBooleanVarData, + GeneralBooleanVarData, BooleanVarList, ScalarBooleanVar, ) diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index bf9d6159754..287851a7f7e 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -194,7 +194,7 @@ def _associated_binary_mapper(encode, val): return val -class _GeneralBooleanVarData(BooleanVarData): +class GeneralBooleanVarData(BooleanVarData): """ This class defines the data for a single Boolean variable. @@ -271,13 +271,13 @@ def stale(self, val): def get_associated_binary(self): """Get the binary _VarData associated with this - _GeneralBooleanVarData""" + GeneralBooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary _VarData to this _GeneralBooleanVarData""" + """Associate a binary _VarData to this GeneralBooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) @@ -300,6 +300,11 @@ def associate_binary_var(self, binary_var): self._associated_binary = weakref_ref(binary_var) +class _GeneralBooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralBooleanVarData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Logical decision variables.") class BooleanVar(IndexedComponent): """A logical variable, which may be defined over an index. @@ -314,7 +319,7 @@ class BooleanVar(IndexedComponent): to True. """ - _ComponentDataClass = _GeneralBooleanVarData + _ComponentDataClass = GeneralBooleanVarData def __new__(cls, *args, **kwds): if cls != BooleanVar: @@ -506,11 +511,11 @@ def _pprint(self): ) -class ScalarBooleanVar(_GeneralBooleanVarData, BooleanVar): +class ScalarBooleanVar(GeneralBooleanVarData, BooleanVar): """A single variable.""" def __init__(self, *args, **kwd): - _GeneralBooleanVarData.__init__(self, component=self) + GeneralBooleanVarData.__init__(self, component=self) BooleanVar.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -526,7 +531,7 @@ def __init__(self, *args, **kwd): def value(self): """Return the value for this variable.""" if self._constructed: - return _GeneralBooleanVarData.value.fget(self) + return GeneralBooleanVarData.value.fget(self) raise ValueError( "Accessing the value of variable '%s' " "before the Var has been constructed (there " @@ -537,7 +542,7 @@ def value(self): def value(self, val): """Set the value for this variable.""" if self._constructed: - return _GeneralBooleanVarData.value.fset(self, val) + return GeneralBooleanVarData.value.fset(self, val) raise ValueError( "Setting the value of variable '%s' " "before the Var has been constructed (there " @@ -546,7 +551,7 @@ def value(self, val): @property def domain(self): - return _GeneralBooleanVarData.domain.fget(self) + return GeneralBooleanVarData.domain.fget(self) def fix(self, value=NOTSET, skip_validation=False): """ @@ -554,7 +559,7 @@ def fix(self, value=NOTSET, skip_validation=False): indicating the variable should be fixed at its current value. """ if self._constructed: - return _GeneralBooleanVarData.fix(self, value, skip_validation) + return GeneralBooleanVarData.fix(self, value, skip_validation) raise ValueError( "Fixing variable '%s' " "before the Var has been constructed (there " @@ -564,7 +569,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Sets the fixed indicator to False.""" if self._constructed: - return _GeneralBooleanVarData.unfix(self) + return GeneralBooleanVarData.unfix(self) raise ValueError( "Freeing variable '%s' " "before the Var has been constructed (there " diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index dcaf976356b..1317c928686 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, - # _ParamData,_GeneralVarData, _GeneralBooleanVarData, DisjunctionData, + # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! From 80c064ac99ac5870cc1439f4cd3405f7565a3516 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:42:40 -0600 Subject: [PATCH 0739/1178] Renamed _GeneralConstraintData -> GeneralConstraintData --- pyomo/contrib/appsi/base.py | 48 +++++++++---------- pyomo/contrib/appsi/fbbt.py | 6 +-- pyomo/contrib/appsi/solvers/cbc.py | 6 +-- pyomo/contrib/appsi/solvers/cplex.py | 10 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 16 +++---- pyomo/contrib/appsi/solvers/highs.py | 6 +-- pyomo/contrib/appsi/solvers/ipopt.py | 10 ++-- pyomo/contrib/appsi/solvers/wntr.py | 6 +-- pyomo/contrib/appsi/writers/lp_writer.py | 6 +-- pyomo/contrib/appsi/writers/nl_writer.py | 6 +-- pyomo/contrib/solver/base.py | 10 ++-- pyomo/contrib/solver/gurobi.py | 16 +++---- pyomo/contrib/solver/persistent.py | 12 ++--- pyomo/contrib/solver/solution.py | 14 +++--- .../contrib/solver/tests/unit/test_results.py | 6 +-- pyomo/core/base/constraint.py | 27 ++++++----- pyomo/core/tests/unit/test_con.py | 4 +- pyomo/core/tests/unit/test_dict_objects.py | 4 +- pyomo/core/tests/unit/test_list_objects.py | 4 +- pyomo/gdp/tests/common_tests.py | 4 +- pyomo/gdp/tests/test_bigm.py | 4 +- pyomo/gdp/tests/test_hull.py | 4 +- .../plugins/solvers/gurobi_persistent.py | 10 ++-- 23 files changed, 121 insertions(+), 118 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index b4ade16a597..d5982fc72e6 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -21,7 +21,7 @@ Tuple, MutableMapping, ) -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param @@ -216,8 +216,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -235,8 +235,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Returns a dictionary mapping constraint to slack. @@ -319,8 +319,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' @@ -336,8 +336,8 @@ def get_duals( return duals def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._slacks is None: raise RuntimeError( 'Solution loader does not currently have valid slacks. Please ' @@ -731,8 +731,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Declare sign convention in docstring here. @@ -752,8 +752,8 @@ def get_duals( ) def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Parameters ---------- @@ -807,7 +807,7 @@ def add_params(self, params: List[_ParamData]): pass @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): pass @abc.abstractmethod @@ -823,7 +823,7 @@ def remove_params(self, params: List[_ParamData]): pass @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): pass @abc.abstractmethod @@ -857,14 +857,14 @@ def get_primals(self, vars_to_load=None): return self._solver.get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_duals(cons_to_load=cons_to_load) def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_slacks(cons_to_load=cons_to_load) @@ -984,7 +984,7 @@ def add_params(self, params: List[_ParamData]): self._add_params(params) @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): pass def _check_for_new_vars(self, variables: List[_GeneralVarData]): @@ -1004,7 +1004,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): all_fixed_vars = dict() for con in cons: if con in self._named_expressions: @@ -1132,10 +1132,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): pass - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -1334,7 +1334,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) + c.ctype is None and isinstance(c, GeneralConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index c6bbdb5bf3b..121557414b3 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -20,7 +20,7 @@ from typing import List, Optional from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData @@ -154,7 +154,7 @@ def _add_params(self, params: List[_ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_fbbt_constraints( self._cmodel, self._pyomo_expr_types, @@ -175,7 +175,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): if self._symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 57bbf1b4c21..cc7327df11c 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -27,7 +27,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData @@ -170,7 +170,7 @@ def add_variables(self, variables: List[_GeneralVarData]): def add_params(self, params: List[_ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -182,7 +182,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): def remove_params(self, params: List[_ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 2e04a979fda..222c466fb99 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -23,7 +23,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData @@ -185,7 +185,7 @@ def add_variables(self, variables: List[_GeneralVarData]): def add_params(self, params: List[_ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -197,7 +197,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): def remove_params(self, params: List[_ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -389,8 +389,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 1e18862e3bd..e20168034c6 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -24,7 +24,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import Var, _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -579,7 +579,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -735,7 +735,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1195,7 +1195,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1272,7 +1272,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1304,7 +1304,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1425,7 +1425,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The cut to add """ if not con.active: @@ -1510,7 +1510,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3612b9d5014..7773d0624b2 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -21,7 +21,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant @@ -376,7 +376,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -462,7 +462,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 19ec5f8031c..75ebb10f719 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -29,7 +29,7 @@ from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData @@ -234,7 +234,7 @@ def add_variables(self, variables: List[_GeneralVarData]): def add_params(self, params: List[_ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -246,7 +246,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): def remove_params(self, params: List[_ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -534,9 +534,7 @@ def get_primals( res[v] = self._primal_sol[v] return res - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ): + def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None): if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 928eda2b514..c11536e2e6f 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -42,7 +42,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import @@ -278,7 +278,7 @@ def _add_params(self, params: List[_ParamData]): setattr(self._solver_model, pname, wntr_p) self._pyomo_param_to_solver_param_map[id(p)] = wntr_p - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): aml = wntr.sim.aml.aml for con in cons: if not con.equality: @@ -294,7 +294,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): self._pyomo_con_to_solver_con_map[con] = wntr_con self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for con in cons: solver_con = self._pyomo_con_to_solver_con_map[con] delattr(self._solver_model, solver_con.name) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 518be5fac99..39298bd1a61 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData @@ -99,14 +99,14 @@ def _add_params(self, params: List[_ParamData]): cp.value = p.value self._pyomo_param_to_solver_param_map[id(p)] = cp - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_lp_constraints(cons, self) def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for c in cons: cc = self._pyomo_con_to_solver_con_map.pop(c) self._writer.remove_constraint(cc) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 75b026ab521..3e13ef4077a 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData @@ -111,7 +111,7 @@ def _add_params(self, params: List[_ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_nl_constraints( self._writer, self._expr_types, @@ -130,7 +130,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): if self.config.symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 4b7da383a57..4b7d8f35ddc 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,7 +14,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import BlockData @@ -232,8 +232,8 @@ def _get_primals( ) def _get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Declare sign convention in docstring here. @@ -294,7 +294,7 @@ def add_parameters(self, params: List[_ParamData]): """ @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): """ Add constraints to the model """ @@ -318,7 +318,7 @@ def remove_parameters(self, params: List[_ParamData]): """ @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): """ Remove constraints from the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index d0ac0d80f45..cc95c0c5f0d 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -23,7 +23,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -555,7 +555,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -711,7 +711,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1125,7 +1125,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1202,7 +1202,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1234,7 +1234,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1355,7 +1355,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The cut to add """ if not con.active: @@ -1440,7 +1440,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 4b1a7c58dcd..9b63e05ce46 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -12,7 +12,7 @@ import abc from typing import List -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData, Param @@ -84,7 +84,7 @@ def add_parameters(self, params: List[_ParamData]): self._add_parameters(params) @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): pass def _check_for_new_vars(self, variables: List[_GeneralVarData]): @@ -104,7 +104,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): all_fixed_vars = {} for con in cons: if con in self._named_expressions: @@ -209,10 +209,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): pass - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -384,7 +384,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) + c.ctype is None and isinstance(c, GeneralConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 32e84d2abca..e8c4631e7fd 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -12,7 +12,7 @@ import abc from typing import Sequence, Dict, Optional, Mapping, NoReturn -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap @@ -67,8 +67,8 @@ def get_primals( """ def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -121,8 +121,8 @@ def get_primals(self, vars_to_load=None): return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: self._assert_solution_still_valid() return self._solver._get_duals(cons_to_load=cons_to_load) @@ -205,8 +205,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 74404aaba4c..38d6a540836 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -15,7 +15,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results @@ -67,8 +67,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index cbae828a459..3455d2dde3c 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -285,7 +285,7 @@ class _ConstraintData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _GeneralConstraintData(ConstraintData): +class GeneralConstraintData(ConstraintData): """ This class defines the data for a single general constraint. @@ -684,6 +684,11 @@ def set_value(self, expr): ) +class _GeneralConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralConstraintData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("General constraint expressions.") class Constraint(ActiveIndexedComponent): """ @@ -726,7 +731,7 @@ class Constraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralConstraintData + _ComponentDataClass = GeneralConstraintData class Infeasible(object): pass @@ -884,14 +889,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarConstraint(_GeneralConstraintData, Constraint): +class ScalarConstraint(GeneralConstraintData, Constraint): """ ScalarConstraint is the implementation representing a single, non-indexed constraint. """ def __init__(self, *args, **kwds): - _GeneralConstraintData.__init__(self, component=self, expr=None) + GeneralConstraintData.__init__(self, component=self, expr=None) Constraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -915,7 +920,7 @@ def body(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.body.fget(self) + return GeneralConstraintData.body.fget(self) @property def lower(self): @@ -927,7 +932,7 @@ def lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.lower.fget(self) + return GeneralConstraintData.lower.fget(self) @property def upper(self): @@ -939,7 +944,7 @@ def upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.upper.fget(self) + return GeneralConstraintData.upper.fget(self) @property def equality(self): @@ -951,7 +956,7 @@ def equality(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.equality.fget(self) + return GeneralConstraintData.equality.fget(self) @property def strict_lower(self): @@ -963,7 +968,7 @@ def strict_lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.strict_lower.fget(self) + return GeneralConstraintData.strict_lower.fget(self) @property def strict_upper(self): @@ -975,7 +980,7 @@ def strict_upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.strict_upper.fget(self) + return GeneralConstraintData.strict_upper.fget(self) def clear(self): self._data = {} @@ -1040,7 +1045,7 @@ def add(self, index, expr): return self.__setitem__(index, expr) @overload - def __getitem__(self, index) -> _GeneralConstraintData: ... + def __getitem__(self, index) -> GeneralConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index 2fa6c24de9c..26ccc7944a7 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -44,7 +44,7 @@ InequalityExpression, RangedExpression, ) -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData class TestConstraintCreation(unittest.TestCase): @@ -1074,7 +1074,7 @@ def test_setitem(self): m.c[2] = m.x**2 <= 4 self.assertEqual(len(m.c), 1) self.assertEqual(list(m.c.keys()), [2]) - self.assertIsInstance(m.c[2], _GeneralConstraintData) + self.assertIsInstance(m.c[2], GeneralConstraintData) self.assertEqual(m.c[2].upper, 4) m.c[3] = Constraint.Skip diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 8260f1ae320..a13e0f25ac8 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -18,7 +18,7 @@ ExpressionDict, ) from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.expression import _GeneralExpressionData @@ -375,7 +375,7 @@ def setUp(self): class TestConstraintDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ConstraintDict - _cdatatype = _GeneralConstraintData + _cdatatype = GeneralConstraintData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 3eb2e279964..e9c1cceb701 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -18,7 +18,7 @@ XExpressionList, ) from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.expression import _GeneralExpressionData @@ -392,7 +392,7 @@ def setUp(self): class TestConstraintList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XConstraintList - _cdatatype = _GeneralConstraintData + _cdatatype = GeneralConstraintData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index e15a7c66d8a..233c3ca9c09 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -953,7 +953,7 @@ def check_disjunction_data_target(self, transformation): self.assertIsInstance(transBlock, Block) self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) self.assertIsInstance( - transBlock.disjunction_xor[2], constraint._GeneralConstraintData + transBlock.disjunction_xor[2], constraint.GeneralConstraintData ) self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block) self.assertEqual(len(transBlock.relaxedDisjuncts), 3) @@ -963,7 +963,7 @@ def check_disjunction_data_target(self, transformation): m, targets=[m.disjunction[1]] ) self.assertIsInstance( - m.disjunction[1].algebraic_constraint, constraint._GeneralConstraintData + m.disjunction[1].algebraic_constraint, constraint.GeneralConstraintData ) transBlock = m.component("_pyomo_gdp_%s_reformulation_4" % transformation) self.assertIsInstance(transBlock, Block) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index d5dcef3ba58..efef4c5fb1f 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1323,8 +1323,8 @@ def test_do_not_transform_deactivated_constraintDatas(self): self.assertEqual(len(cons_list), 2) lb = cons_list[0] ub = cons_list[1] - self.assertIsInstance(lb, constraint._GeneralConstraintData) - self.assertIsInstance(ub, constraint._GeneralConstraintData) + self.assertIsInstance(lb, constraint.GeneralConstraintData) + self.assertIsInstance(ub, constraint.GeneralConstraintData) def checkMs( self, m, disj1c1lb, disj1c1ub, disj1c2lb, disj1c2ub, disj2c1ub, disj2c2ub diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 55edf244731..6093e01dc25 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1253,11 +1253,11 @@ def check_second_iteration(self, model): orig = model.component("_pyomo_gdp_hull_reformulation") self.assertIsInstance( model.disjunctionList[1].algebraic_constraint, - constraint._GeneralConstraintData, + constraint.GeneralConstraintData, ) self.assertIsInstance( model.disjunctionList[0].algebraic_constraint, - constraint._GeneralConstraintData, + constraint.GeneralConstraintData, ) self.assertFalse(model.disjunctionList[1].active) self.assertFalse(model.disjunctionList[0].active) diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 4522a2151c3..101a5340ea9 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -157,7 +157,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -384,7 +384,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -431,7 +431,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -569,7 +569,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The cut to add """ if not con.active: @@ -647,7 +647,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The lazy constraint to add """ if not con.active: From da96ac3f59b5336be1d805cd8ad60e8b9707b8cd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:43:27 -0600 Subject: [PATCH 0740/1178] Renamed _GeneralExpressionData -> GeneralExpressionData --- pyomo/contrib/cp/repn/docplex_writer.py | 6 ++--- .../logical_to_disjunctive_walker.py | 4 +-- pyomo/contrib/fbbt/fbbt.py | 10 +++---- pyomo/contrib/latex_printer/latex_printer.py | 4 +-- pyomo/core/base/component.py | 2 +- pyomo/core/base/expression.py | 27 +++++++++++-------- pyomo/core/base/objective.py | 6 ++--- pyomo/core/tests/unit/test_dict_objects.py | 4 +-- pyomo/core/tests/unit/test_expression.py | 8 +++--- pyomo/core/tests/unit/test_list_objects.py | 4 +-- pyomo/dae/integral.py | 4 +-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/standard_repn.py | 6 ++--- 13 files changed, 46 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 510bbf4e398..00b187a585e 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -63,7 +63,7 @@ GeneralBooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar import pyomo.core.expr as EXPR @@ -949,7 +949,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): BeforeExpression: _handle_before_expression_node, AtExpression: _handle_at_expression_node, AlwaysIn: _handle_always_in_node, - _GeneralExpressionData: _handle_named_expression_node, + GeneralExpressionData: _handle_named_expression_node, ScalarExpression: _handle_named_expression_node, } _var_handles = { @@ -966,7 +966,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarBooleanVar: _before_boolean_var, GeneralBooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, - _GeneralExpressionData: _before_named_expression, + GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 548078f55f8..b4fb5e26900 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -27,7 +27,7 @@ value, ) import pyomo.core.base.boolean_var as BV -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -217,7 +217,7 @@ def _dispatch_atmost(visitor, node, *args): # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var _before_child_dispatcher[_GeneralVarData] = _dispatch_var -_before_child_dispatcher[_GeneralExpressionData] = _dispatch_expression +_before_child_dispatcher[GeneralExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index bde33b3caa0..86f94506841 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression +from pyomo.core.base.expression import GeneralExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( @@ -340,7 +340,7 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): Parameters ---------- visitor: _FBBTVisitorLeafToRoot - node: pyomo.core.base.expression._GeneralExpressionData + node: pyomo.core.base.expression.GeneralExpressionData expr: GeneralExpression arg """ bnds_dict = visitor.bnds_dict @@ -366,7 +366,7 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): numeric_expr.UnaryFunctionExpression: _prop_bnds_leaf_to_root_UnaryFunctionExpression, numeric_expr.LinearExpression: _prop_bnds_leaf_to_root_SumExpression, numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, - _GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, + GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, ScalarExpression: _prop_bnds_leaf_to_root_GeneralExpression, }, ) @@ -904,7 +904,7 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): Parameters ---------- - node: pyomo.core.base.expression._GeneralExpressionData + node: pyomo.core.base.expression.GeneralExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -945,7 +945,7 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): ) _prop_bnds_root_to_leaf_map[numeric_expr.AbsExpression] = _prop_bnds_root_to_leaf_abs -_prop_bnds_root_to_leaf_map[_GeneralExpressionData] = ( +_prop_bnds_root_to_leaf_map[GeneralExpressionData] = ( _prop_bnds_root_to_leaf_GeneralExpression ) _prop_bnds_root_to_leaf_map[ScalarExpression] = ( diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 42fc9083953..0e9e379eb21 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -34,7 +34,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( @@ -399,7 +399,7 @@ def __init__(self): EqualityExpression: handle_equality_node, InequalityExpression: handle_inequality_node, RangedExpression: handle_ranged_inequality_node, - _GeneralExpressionData: handle_named_expression_node, + GeneralExpressionData: handle_named_expression_node, ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 1317c928686..6fd82f80ad4 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -803,7 +803,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, - # _GeneralExpressionData, _LogicalConstraintData, + # GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index e21613fcbb1..f5376381b2d 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -148,7 +148,7 @@ class _ExpressionData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _GeneralExpressionDataImpl(ExpressionData): +class GeneralExpressionDataImpl(ExpressionData): """ An object that defines an expression that is never cloned @@ -240,7 +240,7 @@ def __ipow__(self, other): return numeric_expr._pow_dispatcher[e.__class__, other.__class__](e, other) -class _GeneralExpressionData(_GeneralExpressionDataImpl, ComponentData): +class GeneralExpressionData(GeneralExpressionDataImpl, ComponentData): """ An object that defines an expression that is never cloned @@ -258,12 +258,17 @@ class _GeneralExpressionData(_GeneralExpressionDataImpl, ComponentData): __slots__ = ('_args_',) def __init__(self, expr=None, component=None): - _GeneralExpressionDataImpl.__init__(self, expr) + GeneralExpressionDataImpl.__init__(self, expr) # Inlining ComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET +class _GeneralExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralExpressionData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "Named expressions that can be used in other expressions." ) @@ -280,7 +285,7 @@ class Expression(IndexedComponent): doc Text describing this component. """ - _ComponentDataClass = _GeneralExpressionData + _ComponentDataClass = GeneralExpressionData # This seems like a copy-paste error, and should be renamed/removed NoConstraint = IndexedComponent.Skip @@ -407,9 +412,9 @@ def construct(self, data=None): timer.report() -class ScalarExpression(_GeneralExpressionData, Expression): +class ScalarExpression(GeneralExpressionData, Expression): def __init__(self, *args, **kwds): - _GeneralExpressionData.__init__(self, expr=None, component=self) + GeneralExpressionData.__init__(self, expr=None, component=self) Expression.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -432,7 +437,7 @@ def __call__(self, exception=True): def expr(self): """Return expression on this expression.""" if self._constructed: - return _GeneralExpressionData.expr.fget(self) + return GeneralExpressionData.expr.fget(self) raise ValueError( "Accessing the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -450,7 +455,7 @@ def clear(self): def set_value(self, expr): """Set the expression on this expression.""" if self._constructed: - return _GeneralExpressionData.set_value(self, expr) + return GeneralExpressionData.set_value(self, expr) raise ValueError( "Setting the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -460,7 +465,7 @@ def set_value(self, expr): def is_constant(self): """A boolean indicating whether this expression is constant.""" if self._constructed: - return _GeneralExpressionData.is_constant(self) + return GeneralExpressionData.is_constant(self) raise ValueError( "Accessing the is_constant flag of Expression '%s' " "before the Expression has been constructed (there " @@ -470,7 +475,7 @@ def is_constant(self): def is_fixed(self): """A boolean indicating whether this expression is fixed.""" if self._constructed: - return _GeneralExpressionData.is_fixed(self) + return GeneralExpressionData.is_fixed(self) raise ValueError( "Accessing the is_fixed flag of Expression '%s' " "before the Expression has been constructed (there " @@ -514,6 +519,6 @@ def add(self, index, expr): """Add an expression with a given index.""" if (type(expr) is tuple) and (expr == Expression.Skip): return None - cdata = _GeneralExpressionData(expr, component=self) + cdata = GeneralExpressionData(expr, component=self) self._data[index] = cdata return cdata diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index d259358dcd7..5cd1a1f93eb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -28,7 +28,7 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import ExpressionData, _GeneralExpressionDataImpl +from pyomo.core.base.expression import ExpressionData, GeneralExpressionDataImpl from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, @@ -120,7 +120,7 @@ def set_sense(self, sense): class _GeneralObjectiveData( - _GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData + GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData ): """ This class defines the data for a single objective. @@ -147,7 +147,7 @@ class _GeneralObjectiveData( __slots__ = ("_sense", "_args_") def __init__(self, expr=None, sense=minimize, component=None): - _GeneralExpressionDataImpl.__init__(self, expr) + GeneralExpressionDataImpl.__init__(self, expr) # Inlining ActiveComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index a13e0f25ac8..c82103cefb1 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import GeneralExpressionData class _TestComponentDictBase(object): @@ -360,7 +360,7 @@ def setUp(self): class TestExpressionDict(_TestComponentDictBase, unittest.TestCase): _ctype = ExpressionDict - _cdatatype = _GeneralExpressionData + _cdatatype = GeneralExpressionData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index 678df4c01a8..bf3ce0c2179 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -29,7 +29,7 @@ value, sum_product, ) -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import GeneralExpressionData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -515,10 +515,10 @@ def test_implicit_definition(self): model.E = Expression(model.idx) self.assertEqual(len(model.E), 3) expr = model.E[1] - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), GeneralExpressionData) model.E[1] = None self.assertIs(expr, model.E[1]) - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), GeneralExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) @@ -537,7 +537,7 @@ def test_explicit_skip_definition(self): model.E[1] = None expr = model.E[1] - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), GeneralExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index e9c1cceb701..b8e97b464fe 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import GeneralExpressionData class _TestComponentListBase(object): @@ -377,7 +377,7 @@ def setUp(self): class TestExpressionList(_TestComponentListBase, unittest.TestCase): _ctype = XExpressionList - _cdatatype = _GeneralExpressionData + _cdatatype = GeneralExpressionData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index 41114296a93..f767e31f18c 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -14,7 +14,7 @@ from pyomo.core.base.indexed_component import rule_wrapper from pyomo.core.base.expression import ( Expression, - _GeneralExpressionData, + GeneralExpressionData, ScalarExpression, IndexedExpression, ) @@ -151,7 +151,7 @@ class ScalarIntegral(ScalarExpression, Integral): """ def __init__(self, *args, **kwds): - _GeneralExpressionData.__init__(self, None, component=self) + GeneralExpressionData.__init__(self, None, component=self) Integral.__init__(self, *args, **kwds) def clear(self): diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 29d841248da..a0dc09e2aa6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -70,7 +70,7 @@ ) from pyomo.core.base.component import ActiveComponent from pyomo.core.base.constraint import ConstraintData -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.objective import ( ScalarObjective, _GeneralObjectiveData, diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 70368dd3d7e..cf2ba334d6c 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -21,7 +21,7 @@ from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData from pyomo.core.kernel.expression import expression, noclone @@ -1148,7 +1148,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra Var: _collect_var, variable: _collect_var, IVariable: _collect_var, - _GeneralExpressionData: _collect_identity, + GeneralExpressionData: _collect_identity, ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, @@ -1547,7 +1547,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): Var : _linear_collect_var, variable : _linear_collect_var, IVariable : _linear_collect_var, - _GeneralExpressionData : _linear_collect_identity, + GeneralExpressionData : _linear_collect_identity, ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, From cd5db6637bdd16a72d8260459c6b7757dd6c3cf0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:43:42 -0600 Subject: [PATCH 0741/1178] Renamed _GeneralLogicalConstraintData -> GeneralLogicalConstraintData --- pyomo/core/base/component.py | 2 +- pyomo/core/base/logical_constraint.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 6fd82f80ad4..720373db809 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, _LogicalConstraintData, - # _GeneralLogicalConstraintData, _GeneralObjectiveData, + # GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 3a7bca75960..9af99c9ce5c 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -99,7 +99,7 @@ def get_value(self): raise NotImplementedError -class _GeneralLogicalConstraintData(_LogicalConstraintData): +class GeneralLogicalConstraintData(_LogicalConstraintData): """ This class defines the data for a single general logical constraint. @@ -173,6 +173,11 @@ def get_value(self): return self._expr +class _GeneralLogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralLogicalConstraintData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("General logical constraints.") class LogicalConstraint(ActiveIndexedComponent): """ @@ -215,7 +220,7 @@ class LogicalConstraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralLogicalConstraintData + _ComponentDataClass = GeneralLogicalConstraintData class Infeasible(object): pass @@ -409,14 +414,14 @@ def _check_skip_add(self, index, expr): return expr -class ScalarLogicalConstraint(_GeneralLogicalConstraintData, LogicalConstraint): +class ScalarLogicalConstraint(GeneralLogicalConstraintData, LogicalConstraint): """ ScalarLogicalConstraint is the implementation representing a single, non-indexed logical constraint. """ def __init__(self, *args, **kwds): - _GeneralLogicalConstraintData.__init__(self, component=self, expr=None) + GeneralLogicalConstraintData.__init__(self, component=self, expr=None) LogicalConstraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -436,7 +441,7 @@ def body(self): "an expression. There is currently " "nothing to access." % self.name ) - return _GeneralLogicalConstraintData.body.fget(self) + return GeneralLogicalConstraintData.body.fget(self) raise ValueError( "Accessing the body of logical constraint '%s' " "before the LogicalConstraint has been constructed (there " From 6e9d84cb43e1f327fc230c0711769cea598f3d03 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:45:29 -0600 Subject: [PATCH 0742/1178] Renamed _GeneralObjectiveData -> GeneralObjectiveData --- pyomo/contrib/appsi/base.py | 8 ++++---- pyomo/contrib/appsi/fbbt.py | 6 +++--- pyomo/contrib/appsi/solvers/cbc.py | 4 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- pyomo/contrib/appsi/writers/lp_writer.py | 4 ++-- pyomo/contrib/appsi/writers/nl_writer.py | 4 ++-- .../contrib/community_detection/detection.py | 4 ++-- pyomo/contrib/latex_printer/latex_printer.py | 4 ++-- pyomo/contrib/solver/base.py | 4 ++-- pyomo/contrib/solver/persistent.py | 6 +++--- pyomo/core/base/component.py | 2 +- pyomo/core/base/objective.py | 19 ++++++++++++------- pyomo/core/tests/unit/test_dict_objects.py | 4 ++-- pyomo/core/tests/unit/test_list_objects.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/standard_repn.py | 6 +++--- 17 files changed, 47 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index d5982fc72e6..b1538ef1a35 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -26,7 +26,7 @@ from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.block import BlockData, Block -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs @@ -831,7 +831,7 @@ def remove_block(self, block: BlockData): pass @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): pass @abc.abstractmethod @@ -1054,10 +1054,10 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): pass - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 121557414b3..ca178a49b00 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -22,7 +22,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize +from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -224,13 +224,13 @@ def update_params(self): cp = self._param_map[p_id] cp.value = p.value - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): if self._symbolic_solver_labels: if self._objective is not None: self._symbol_map.removeSymbol(self._objective) super().set_objective(obj) - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): if obj is None: ce = cmodel.Constant(0) sense = 0 diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index cc7327df11c..e73d080c02b 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -30,7 +30,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -188,7 +188,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[_GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 222c466fb99..ffca656735e 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer import sys import time @@ -203,7 +203,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[_GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 75ebb10f719..97d76a9ecb1 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -32,7 +32,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -252,7 +252,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[_GeneralVarData]): diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 39298bd1a61..696b1c16d61 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -147,7 +147,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): cobj = cmodel.process_lp_objective( self._expr_types, obj, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 3e13ef4077a..33d7c59f08f 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -180,7 +180,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): if obj is None: const = cmodel.Constant(0) lin_vars = list() diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index 5bf8187a243..af87fa5eb8b 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -31,7 +31,7 @@ Objective, ConstraintList, ) -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.expr.visitor import replace_expressions, identify_variables from pyomo.contrib.community_detection.community_graph import generate_model_graph from pyomo.common.dependencies import networkx as nx @@ -750,7 +750,7 @@ def generate_structured_model(self): # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives # grouped together) if self.with_objective and isinstance( - stored_constraint, (_GeneralObjectiveData, Objective) + stored_constraint, (GeneralObjectiveData, Objective) ): # If the constraint is actually an objective, we add it to the block as an objective new_objective = Objective( diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 0e9e379eb21..5a2365f9544 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -35,7 +35,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData +from pyomo.core.base.objective import ScalarObjective, GeneralObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( GetItemExpression, @@ -403,7 +403,7 @@ def __init__(self): ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, - _GeneralObjectiveData: handle_named_expression_node, + GeneralObjectiveData: handle_named_expression_node, _GeneralVarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 4b7d8f35ddc..0a12f572e5f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -18,7 +18,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import BlockData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning @@ -276,7 +276,7 @@ def set_instance(self, model): """ @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): """ Set current objective for the model """ diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 9b63e05ce46..97a4067e78b 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -16,7 +16,7 @@ from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant @@ -149,10 +149,10 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): pass - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 720373db809..1fd30f4212e 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, _LogicalConstraintData, - # GeneralLogicalConstraintData, _GeneralObjectiveData, + # GeneralLogicalConstraintData, GeneralObjectiveData, # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 5cd1a1f93eb..b89214377ab 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -119,7 +119,7 @@ def set_sense(self, sense): raise NotImplementedError -class _GeneralObjectiveData( +class GeneralObjectiveData( GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData ): """ @@ -192,6 +192,11 @@ def set_sense(self, sense): ) +class _GeneralObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralObjectiveData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Expressions that are minimized or maximized.") class Objective(ActiveIndexedComponent): """ @@ -240,7 +245,7 @@ class Objective(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralObjectiveData + _ComponentDataClass = GeneralObjectiveData NoObjective = ActiveIndexedComponent.Skip def __new__(cls, *args, **kwds): @@ -389,14 +394,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarObjective(_GeneralObjectiveData, Objective): +class ScalarObjective(GeneralObjectiveData, Objective): """ ScalarObjective is the implementation representing a single, non-indexed objective. """ def __init__(self, *args, **kwd): - _GeneralObjectiveData.__init__(self, expr=None, component=self) + GeneralObjectiveData.__init__(self, expr=None, component=self) Objective.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -432,7 +437,7 @@ def expr(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return _GeneralObjectiveData.expr.fget(self) + return GeneralObjectiveData.expr.fget(self) raise ValueError( "Accessing the expression of objective '%s' " "before the Objective has been constructed (there " @@ -455,7 +460,7 @@ def sense(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return _GeneralObjectiveData.sense.fget(self) + return GeneralObjectiveData.sense.fget(self) raise ValueError( "Accessing the sense of objective '%s' " "before the Objective has been constructed (there " @@ -498,7 +503,7 @@ def set_sense(self, sense): if self._constructed: if len(self._data) == 0: self._data[None] = self - return _GeneralObjectiveData.set_sense(self, sense) + return GeneralObjectiveData.set_sense(self, sense) raise ValueError( "Setting the sense of objective '%s' " "before the Objective has been constructed (there " diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index c82103cefb1..6dd8a21e2b4 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -384,7 +384,7 @@ def setUp(self): class TestObjectiveDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ObjectiveDict - _cdatatype = _GeneralObjectiveData + _cdatatype = GeneralObjectiveData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index b8e97b464fe..1609f97af90 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -401,7 +401,7 @@ def setUp(self): class TestObjectiveList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XObjectiveList - _cdatatype = _GeneralObjectiveData + _cdatatype = GeneralObjectiveData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a0dc09e2aa6..e1afb5720f3 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -73,7 +73,7 @@ from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.objective import ( ScalarObjective, - _GeneralObjectiveData, + GeneralObjectiveData, _ObjectiveData, ) from pyomo.core.base.suffix import SuffixFinder diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index cf2ba334d6c..442e4677dbd 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -19,7 +19,7 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant -from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective +from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value @@ -1154,7 +1154,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra noclone: _collect_identity, ExpressionData: _collect_identity, Expression: _collect_identity, - _GeneralObjectiveData: _collect_identity, + GeneralObjectiveData: _collect_identity, ScalarObjective: _collect_identity, objective: _collect_identity, } @@ -1553,7 +1553,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): noclone : _linear_collect_identity, ExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, - _GeneralObjectiveData : _linear_collect_identity, + GeneralObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, objective : _linear_collect_identity, } From aa79e1b4b0e517a28b8bf8360dea9b6a4b91bba3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:48:14 -0600 Subject: [PATCH 0743/1178] Renamed _GeneralVarData -> GeneralVarData --- pyomo/contrib/appsi/base.py | 56 +++++++++---------- pyomo/contrib/appsi/fbbt.py | 10 ++-- pyomo/contrib/appsi/solvers/cbc.py | 16 +++--- pyomo/contrib/appsi/solvers/cplex.py | 16 +++--- pyomo/contrib/appsi/solvers/gurobi.py | 12 ++-- pyomo/contrib/appsi/solvers/highs.py | 8 +-- pyomo/contrib/appsi/solvers/ipopt.py | 16 +++--- pyomo/contrib/appsi/solvers/wntr.py | 8 +-- pyomo/contrib/appsi/writers/lp_writer.py | 8 +-- pyomo/contrib/appsi/writers/nl_writer.py | 8 +-- pyomo/contrib/cp/repn/docplex_writer.py | 4 +- .../logical_to_disjunctive_walker.py | 4 +- pyomo/contrib/latex_printer/latex_printer.py | 10 ++-- pyomo/contrib/parmest/utils/scenario_tree.py | 2 +- pyomo/contrib/solver/base.py | 22 ++++---- pyomo/contrib/solver/gurobi.py | 12 ++-- pyomo/contrib/solver/ipopt.py | 6 +- pyomo/contrib/solver/persistent.py | 18 +++--- pyomo/contrib/solver/solution.py | 22 ++++---- .../contrib/solver/tests/unit/test_results.py | 10 ++-- .../trustregion/tests/test_interface.py | 4 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/var.py | 15 +++-- pyomo/core/expr/calculus/derivatives.py | 6 +- pyomo/core/tests/transform/test_add_slacks.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 6 +- pyomo/core/tests/unit/test_list_objects.py | 6 +- pyomo/core/tests/unit/test_numeric_expr.py | 4 +- pyomo/core/tests/unit/test_reference.py | 12 ++-- pyomo/repn/standard_repn.py | 6 +- .../plugins/solvers/gurobi_persistent.py | 4 +- pyomo/util/report_scaling.py | 4 +- 33 files changed, 173 insertions(+), 168 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index b1538ef1a35..1ce24220bfd 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -23,7 +23,7 @@ ) from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.var import GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import GeneralObjectiveData @@ -180,7 +180,7 @@ def __init__( class SolutionLoaderBase(abc.ABC): def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -197,8 +197,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -256,8 +256,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -303,8 +303,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -353,8 +353,8 @@ def get_slacks( return slacks def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' @@ -709,7 +709,7 @@ def is_persistent(self): return True def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -726,8 +726,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: pass def get_duals( @@ -771,8 +771,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Parameters ---------- @@ -799,7 +799,7 @@ def set_instance(self, model): pass @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod @@ -815,7 +815,7 @@ def add_block(self, block: BlockData): pass @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod @@ -835,7 +835,7 @@ def set_objective(self, obj: GeneralObjectiveData): pass @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod @@ -869,8 +869,8 @@ def get_slacks( return self._solver.get_slacks(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: self._assert_solution_still_valid() return self._solver.get_reduced_costs(vars_to_load=vars_to_load) @@ -954,10 +954,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): pass - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -987,7 +987,7 @@ def add_params(self, params: List[_ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[_GeneralVarData]): + def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars = dict() for v in variables: v_id = id(v) @@ -995,7 +995,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + def _check_to_remove_vars(self, variables: List[GeneralVarData]): vars_to_remove = dict() for v in variables: v_id = id(v) @@ -1174,10 +1174,10 @@ def remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): pass - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -1246,10 +1246,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): pass - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index ca178a49b00..a360d2bce84 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -18,7 +18,7 @@ ) from .cmodel import cmodel, cmodel_available from typing import List, Optional -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData @@ -121,7 +121,7 @@ def set_instance(self, model, symbolic_solver_labels: Optional[bool] = None): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): if self._symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -190,7 +190,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): if self._symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -205,7 +205,7 @@ def _remove_params(self, params: List[_ParamData]): for p in params: del self._param_map[id(p)] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._pyomo_expr_types, variables, @@ -304,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: BlockData, seed_var: _GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: GeneralVarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index e73d080c02b..cd5158d905e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -26,7 +26,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData @@ -164,7 +164,7 @@ def symbol_map(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) def add_params(self, params: List[_ParamData]): @@ -176,7 +176,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[_ParamData]): @@ -191,7 +191,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): self._writer.update_variables(variables) def update_params(self): @@ -440,8 +440,8 @@ def _check_and_escape_options(): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -477,8 +477,8 @@ def get_duals(self, cons_to_load=None): return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index ffca656735e..cdd699105be 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -22,7 +22,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData @@ -179,7 +179,7 @@ def update_config(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) def add_params(self, params: List[_ParamData]): @@ -191,7 +191,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[_ParamData]): @@ -206,7 +206,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): self._writer.update_variables(variables) def update_params(self): @@ -362,8 +362,8 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none @@ -440,8 +440,8 @@ def get_duals( return res def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index e20168034c6..6da59042a80 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import Var, GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -458,7 +458,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): var_names = list() vtypes = list() lbs = list() @@ -759,7 +759,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -774,7 +774,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): def _remove_params(self, params: List[_ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1221,7 +1221,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1256,7 +1256,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 7773d0624b2..ded0092f38b 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,7 +20,7 @@ from pyomo.common.log import LogStream from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -308,7 +308,7 @@ def _process_domain_and_bounds(self, var_id): return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -493,7 +493,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -518,7 +518,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): def _remove_params(self, params: List[_ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 97d76a9ecb1..9ccb58095b1 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -28,7 +28,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData @@ -228,7 +228,7 @@ def set_instance(self, model): self._writer.config.symbolic_solver_labels = self.config.symbolic_solver_labels self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) def add_params(self, params: List[_ParamData]): @@ -240,7 +240,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[_ParamData]): @@ -255,7 +255,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): self._writer.update_variables(variables) def update_params(self): @@ -514,8 +514,8 @@ def _apply_solver(self, timer: HierarchicalTimer): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -551,8 +551,8 @@ def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = No return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index c11536e2e6f..7f633161fe1 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -40,7 +40,7 @@ from pyomo.core.expr.numvalue import native_numeric_types from typing import Dict, Optional, List from pyomo.core.base.block import BlockData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer @@ -239,7 +239,7 @@ def set_instance(self, model): self.add_block(model) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): aml = wntr.sim.aml.aml for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) @@ -302,7 +302,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): del self._pyomo_con_to_solver_con_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for var in variables: v_id = id(var) solver_var = self._pyomo_var_to_solver_var_map[v_id] @@ -322,7 +322,7 @@ def _remove_params(self, params: List[_ParamData]): self._symbol_map.removeSymbol(p) del self._pyomo_param_to_solver_param_map[p_id] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): aml = wntr.sim.aml.aml for var in variables: v_id = id(var) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 696b1c16d61..94af5ba7e93 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData @@ -77,7 +77,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -117,7 +117,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for v in variables: cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] @@ -128,7 +128,7 @@ def _remove_params(self, params: List[_ParamData]): del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 33d7c59f08f..b7dab1d5a3e 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData @@ -78,7 +78,7 @@ def set_instance(self, model): self.set_objective(None) self._set_pyomo_amplfunc_env() - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): if self.config.symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -144,7 +144,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): if self.config.symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -161,7 +161,7 @@ def _remove_params(self, params: List[_ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 00b187a585e..eb50a543160 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -65,7 +65,7 @@ ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData -from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet @@ -961,7 +961,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, ScalarVar: _before_var, - _GeneralVarData: _before_var, + GeneralVarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, GeneralBooleanVarData: _before_boolean_var, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index b4fb5e26900..26b63d020a5 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -29,7 +29,7 @@ import pyomo.core.base.boolean_var as BV from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import ScalarParam, _ParamData -from pyomo.core.base.var import ScalarVar, _GeneralVarData +from pyomo.core.base.var import ScalarVar, GeneralVarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -216,7 +216,7 @@ def _dispatch_atmost(visitor, node, *args): # for the moment, these are all just so we can get good error messages when we # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var -_before_child_dispatcher[_GeneralVarData] = _dispatch_var +_before_child_dispatcher[GeneralVarData] = _dispatch_var _before_child_dispatcher[GeneralExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 5a2365f9544..efcd3016dbf 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -47,7 +47,7 @@ resolve_template, templatize_rule, ) -from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam from pyomo.core.base.set import _SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint @@ -404,7 +404,7 @@ def __init__(self): kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, GeneralObjectiveData: handle_named_expression_node, - _GeneralVarData: handle_var_node, + GeneralVarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, ExternalFunctionExpression: handle_external_function_node, @@ -706,9 +706,9 @@ def latex_printer( temp_comp, temp_indexes = templatize_fcn(pyomo_component) variableList = [] for v in identify_components( - temp_comp, [ScalarVar, _GeneralVarData, IndexedVar] + temp_comp, [ScalarVar, GeneralVarData, IndexedVar] ): - if isinstance(v, _GeneralVarData): + if isinstance(v, GeneralVarData): v_write = v.parent_component() if v_write not in ComponentSet(variableList): variableList.append(v_write) @@ -1275,7 +1275,7 @@ def get_index_names(st, lcm): rep_dict = {} for ky in reversed(list(latex_component_map)): - if isinstance(ky, (pyo.Var, _GeneralVarData)): + if isinstance(ky, (pyo.Var, GeneralVarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index e71f51877b5..1062e4a2bf4 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -25,7 +25,7 @@ def build_vardatalist(self, model, varlist=None): """ - Convert a list of pyomo variables to a list of ScalarVar and _GeneralVarData. If varlist is none, builds a + Convert a list of pyomo variables to a list of ScalarVar and GeneralVarData. If varlist is none, builds a list of all variables in the model. The new list is stored in the vars_to_tighten attribute. By CD Laird Parameters diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 0a12f572e5f..a935a950819 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -15,7 +15,7 @@ import os from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import BlockData from pyomo.core.base.objective import GeneralObjectiveData @@ -195,7 +195,7 @@ def is_persistent(self): return True def _load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -212,19 +212,19 @@ def _load_vars( @abc.abstractmethod def _get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Get mapping of variables to primals. Parameters ---------- - vars_to_load : Optional[Sequence[_GeneralVarData]], optional + vars_to_load : Optional[Sequence[GeneralVarData]], optional Which vars to be populated into the map. The default is None. Returns ------- - Mapping[_GeneralVarData, float] + Mapping[GeneralVarData, float] A map of variables to primals. """ raise NotImplementedError( @@ -251,8 +251,8 @@ def _get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def _get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Parameters ---------- @@ -282,7 +282,7 @@ def set_objective(self, obj: GeneralObjectiveData): """ @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): """ Add variables to the model """ @@ -306,7 +306,7 @@ def add_block(self, block: BlockData): """ @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): """ Remove variables from the model """ @@ -330,7 +330,7 @@ def remove_block(self, block: BlockData): """ @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): """ Update variables on the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index cc95c0c5f0d..353798133db 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -22,7 +22,7 @@ from pyomo.common.config import ConfigValue from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -438,7 +438,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): var_names = list() vtypes = list() lbs = list() @@ -735,7 +735,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -750,7 +750,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): def _remove_parameters(self, params: List[_ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1151,7 +1151,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1186,7 +1186,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5f601b7a9f7..7111ec6e972 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -25,7 +25,7 @@ ) from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase @@ -80,8 +80,8 @@ def __init__( class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 97a4067e78b..aeacc9f87c4 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -14,7 +14,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap @@ -54,10 +54,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): pass - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -87,7 +87,7 @@ def add_parameters(self, params: List[_ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[_GeneralVarData]): + def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars = {} for v in variables: v_id = id(v) @@ -95,7 +95,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + def _check_to_remove_vars(self, variables: List[GeneralVarData]): vars_to_remove = {} for v in variables: v_id = id(v) @@ -250,10 +250,10 @@ def remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): pass - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -309,10 +309,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): pass - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index e8c4631e7fd..3f327c1f280 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -13,7 +13,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap from pyomo.common.errors import DeveloperError @@ -31,7 +31,7 @@ class SolutionLoaderBase(abc.ABC): """ def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -49,8 +49,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -86,8 +86,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -127,8 +127,8 @@ def get_duals( return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: self._assert_solution_still_valid() return self._solver._get_reduced_costs(vars_to_load=vars_to_load) @@ -142,7 +142,7 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: self._nl_info = nl_info def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: if self._nl_info is None: raise RuntimeError( @@ -169,8 +169,8 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 38d6a540836..608af04a0ed 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -16,7 +16,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results from pyomo.contrib.solver import solution @@ -51,8 +51,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -84,8 +84,8 @@ def get_duals( return duals def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index 148caceddd1..64f76eb887d 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -33,7 +33,7 @@ cos, SolverFactory, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.expr.numeric_expr import ExternalFunctionExpression from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.trustregion.interface import TRFInterface @@ -158,7 +158,7 @@ def test_replaceExternalFunctionsWithVariables(self): self.assertIsInstance(k, ExternalFunctionExpression) self.assertIn(str(self.interface.model.x[0]), str(k)) self.assertIn(str(self.interface.model.x[1]), str(k)) - self.assertIsInstance(i, _GeneralVarData) + self.assertIsInstance(i, GeneralVarData) self.assertEqual(i, self.interface.data.ef_outputs[1]) for i, k in self.interface.data.basis_expressions.items(): self.assertEqual(k, 0) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index bb62cb96782..7003cc3d720 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -57,7 +57,7 @@ from pyomo.core.base.check import BuildCheck from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, _VarData, _GeneralVarData, ScalarVar, VarList +from pyomo.core.base.var import Var, _VarData, GeneralVarData, ScalarVar, VarList from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 1fd30f4212e..faf6553be1b 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, _LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, + # _ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 856a2dc0237..0e45ad44225 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -319,7 +319,7 @@ def free(self): return self.unfix() -class _GeneralVarData(_VarData): +class GeneralVarData(_VarData): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -643,6 +643,11 @@ def _process_bound(self, val, bound_type): return val +class _GeneralVarData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralVarData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Decision variables.") class Var(IndexedComponent, IndexedComponent_NDArrayMixin): """A numeric variable, which may be defined over an index. @@ -668,7 +673,7 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): doc (str, optional): Text describing this component. """ - _ComponentDataClass = _GeneralVarData + _ComponentDataClass = GeneralVarData @overload def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... @@ -952,11 +957,11 @@ def _pprint(self): ) -class ScalarVar(_GeneralVarData, Var): +class ScalarVar(GeneralVarData, Var): """A single variable.""" def __init__(self, *args, **kwd): - _GeneralVarData.__init__(self, component=self) + GeneralVarData.__init__(self, component=self) Var.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -1057,7 +1062,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args) -> _GeneralVarData: + def __getitem__(self, args) -> GeneralVarData: try: return super().__getitem__(args) except RuntimeError: diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index ecfdce02fd4..cd23cb16b2c 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -39,11 +39,11 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression The expression to differentiate - wrt: pyomo.core.base.var._GeneralVarData + wrt: pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with - respect to wrt. wrt is normally a _GeneralVarData, but could + respect to wrt. wrt is normally a GeneralVarData, but could also be a _ParamData. wrt and wrt_list cannot both be specified. - wrt_list: list of pyomo.core.base.var._GeneralVarData + wrt_list: list of pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned where the values are the derivatives with respect to the diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index a74a9b75c4f..d66d6fba79e 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -330,7 +330,7 @@ def test_error_for_non_constraint_noniterable_target(self): self.assertRaisesRegex( ValueError, "Expected Constraint or list of Constraints.\n\tReceived " - "", + "", TransformationFactory('core.add_slack_variables').apply_to, m, targets=m.indexedVar[1], diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 6dd8a21e2b4..f2c3cad8cc3 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -17,7 +17,7 @@ ObjectiveDict, ExpressionDict, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -348,10 +348,10 @@ def test_active(self): class TestVarDict(_TestComponentDictBase, unittest.TestCase): - # Note: the updated _GeneralVarData class only takes an optional + # Note: the updated GeneralVarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = VarDict - _cdatatype = lambda self, arg: _GeneralVarData() + _cdatatype = lambda self, arg: GeneralVarData() def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 1609f97af90..fcc83a95a06 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -17,7 +17,7 @@ XObjectiveList, XExpressionList, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -365,10 +365,10 @@ def test_active(self): class TestVarList(_TestComponentListBase, unittest.TestCase): - # Note: the updated _GeneralVarData class only takes an optional + # Note: the updated GeneralVarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = XVarList - _cdatatype = lambda self, arg: _GeneralVarData() + _cdatatype = lambda self, arg: GeneralVarData() def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 968b3acb6a4..8e5e43eac9c 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -112,7 +112,7 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr import expr_common -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.repn import generate_standard_repn from pyomo.core.expr.numvalue import NumericValue @@ -294,7 +294,7 @@ def value_check(self, exp, val): class TestExpression_EvaluateVarData(TestExpression_EvaluateNumericValue): def create(self, val, domain): - tmp = _GeneralVarData() + tmp = GeneralVarData() tmp.domain = domain tmp.value = val return tmp diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index cfd9b99f945..4fa2f4944e9 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -800,8 +800,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Object - 1 : - 2 : + 1 : + 2 : """, ) m.s = Reference(m.x[:, ...], ctype=IndexedComponent) @@ -811,8 +811,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Object - 1 : - 2 : + 1 : + 2 : """, ) @@ -1357,8 +1357,8 @@ def test_pprint_nonfinite_sets_ctypeNone(self): 1 IndexedComponent Declarations ref : Size=2, Index=NonNegativeIntegers, ReferenceTo=v Key : Object - 3 : - 5 : + 3 : + 5 : 2 Declarations: v ref """.strip(), diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 442e4677dbd..907b4a2b115 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -22,7 +22,7 @@ from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value +from pyomo.core.base.var import ScalarVar, Var, GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable @@ -1143,7 +1143,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra # param.Param : _collect_linear_const, # parameter : _collect_linear_const, NumericConstant: _collect_const, - _GeneralVarData: _collect_var, + GeneralVarData: _collect_var, ScalarVar: _collect_var, Var: _collect_var, variable: _collect_var, @@ -1542,7 +1542,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, - _GeneralVarData : _linear_collect_var, + GeneralVarData : _linear_collect_var, ScalarVar : _linear_collect_var, Var : _linear_collect_var, variable : _linear_collect_var, diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 101a5340ea9..8a81aad3d3e 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -192,7 +192,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - con: pyomo.core.base.var._GeneralVarData + con: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -342,7 +342,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 5ae28baa715..265564bf12d 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -13,7 +13,7 @@ import math from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, _GeneralVarData): + if isinstance(_v, GeneralVarData): if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) From edd83c00ba974bf9d73b95b0e084ed0b63eee71a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:48:36 -0600 Subject: [PATCH 0744/1178] Renamed _InfiniteRangeSetData -> InfiniteRangeSetData --- pyomo/core/base/set.py | 23 ++++++++++++++--------- pyomo/core/tests/unit/test_set.py | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8db64620d5c..f885dfeaa16 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -894,7 +894,7 @@ def _get_continuous_interval(self): @property @deprecated("The 'virtual' attribute is no longer supported", version='5.7') def virtual(self): - return isinstance(self, (_AnySet, SetOperator, _InfiniteRangeSetData)) + return isinstance(self, (_AnySet, SetOperator, InfiniteRangeSetData)) @virtual.setter def virtual(self, value): @@ -2608,7 +2608,7 @@ def ord(self, item): ############################################################################ -class _InfiniteRangeSetData(_SetData): +class InfiniteRangeSetData(_SetData): """Data class for a infinite set. This Set implements an interface to an *infinite set* defined by one @@ -2653,8 +2653,13 @@ def ranges(self): return iter(self._ranges) +class _InfiniteRangeSetData(metaclass=RenamedClass): + __renamed__new_class__ = InfiniteRangeSetData + __renamed__version__ = '6.7.2.dev0' + + class FiniteRangeSetData( - _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, _InfiniteRangeSetData + _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, InfiniteRangeSetData ): __slots__ = () @@ -2754,11 +2759,11 @@ def ord(self, item): ) # We must redefine ranges(), bounds(), and domain so that we get the - # _InfiniteRangeSetData version and not the one from + # InfiniteRangeSetData version and not the one from # _FiniteSetMixin. - bounds = _InfiniteRangeSetData.bounds - ranges = _InfiniteRangeSetData.ranges - domain = _InfiniteRangeSetData.domain + bounds = InfiniteRangeSetData.bounds + ranges = InfiniteRangeSetData.ranges + domain = InfiniteRangeSetData.domain class _FiniteRangeSetData(metaclass=RenamedClass): @@ -3228,9 +3233,9 @@ def _pprint(self): ) -class InfiniteScalarRangeSet(_InfiniteRangeSetData, RangeSet): +class InfiniteScalarRangeSet(InfiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): - _InfiniteRangeSetData.__init__(self, component=self) + InfiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index a9b9fb9469b..d669bb38f3b 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -61,7 +61,7 @@ InfiniteSetOf, RangeSet, FiniteRangeSetData, - _InfiniteRangeSetData, + InfiniteRangeSetData, FiniteScalarRangeSet, InfiniteScalarRangeSet, AbstractFiniteScalarRangeSet, @@ -1297,7 +1297,7 @@ def test_is_functions(self): self.assertFalse(i.isdiscrete()) self.assertFalse(i.isfinite()) self.assertFalse(i.isordered()) - self.assertIsInstance(i, _InfiniteRangeSetData) + self.assertIsInstance(i, InfiniteRangeSetData) def test_pprint(self): m = ConcreteModel() From b5c9dbaee26359c842dfeee3d8b962da81a7f513 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:48:49 -0600 Subject: [PATCH 0745/1178] Renamed _InsertionOrderSetData -> InsertionOrderSetData --- pyomo/core/base/set.py | 23 +++++++++++++-------- pyomo/core/tests/unit/test_set.py | 8 +++---- pyomo/core/tests/unit/test_template_expr.py | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index f885dfeaa16..c0e9491ed1c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1642,9 +1642,9 @@ class _OrderedSetData(_OrderedSetMixin, FiniteSetData): In older Pyomo terms, this defines a "concrete" ordered set - that is, a set that "owns" the list of set members. While this class actually implements a set ordered by insertion order, we make the "official" - _InsertionOrderSetData an empty derivative class, so that + InsertionOrderSetData an empty derivative class, so that - issubclass(_SortedSetData, _InsertionOrderSetData) == False + issubclass(_SortedSetData, InsertionOrderSetData) == False Constructor Arguments: component The Set object that owns this data. @@ -1735,7 +1735,7 @@ def ord(self, item): raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name)) -class _InsertionOrderSetData(_OrderedSetData): +class InsertionOrderSetData(_OrderedSetData): """ This class defines the data for a ordered set where the items are ordered in insertion order (similar to Python's OrderedSet. @@ -1756,7 +1756,7 @@ def set_value(self, val): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(val).__name__,) ) - super(_InsertionOrderSetData, self).set_value(val) + super(InsertionOrderSetData, self).set_value(val) def update(self, values): if type(values) in Set._UnorderedInitializers: @@ -1766,7 +1766,12 @@ def update(self, values): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(values).__name__,) ) - super(_InsertionOrderSetData, self).update(values) + super(InsertionOrderSetData, self).update(values) + + +class _InsertionOrderSetData(metaclass=RenamedClass): + __renamed__new_class__ = InsertionOrderSetData + __renamed__version__ = '6.7.2.dev0' class _SortedSetMixin(object): @@ -2035,7 +2040,7 @@ def __new__(cls, *args, **kwds): else: newObj = super(Set, cls).__new__(IndexedSet) if ordered is Set.InsertionOrder: - newObj._ComponentDataClass = _InsertionOrderSetData + newObj._ComponentDataClass = InsertionOrderSetData elif ordered is Set.SortedOrder: newObj._ComponentDataClass = _SortedSetData else: @@ -2363,7 +2368,7 @@ def _pprint(self): _ordered = "Sorted" else: _ordered = "{user}" - elif issubclass(_refClass, _InsertionOrderSetData): + elif issubclass(_refClass, InsertionOrderSetData): _ordered = "Insertion" return ( [ @@ -2405,13 +2410,13 @@ class FiniteSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class OrderedScalarSet(_ScalarOrderedSetMixin, _InsertionOrderSetData, Set): +class OrderedScalarSet(_ScalarOrderedSetMixin, InsertionOrderSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.InsertionOrder) - _InsertionOrderSetData.__init__(self, component=self) + InsertionOrderSetData.__init__(self, component=self) Set.__init__(self, **kwds) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index d669bb38f3b..38870d5213e 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -83,7 +83,7 @@ SetProduct_OrderedSet, _SetData, FiniteSetData, - _InsertionOrderSetData, + InsertionOrderSetData, _SortedSetData, _FiniteSetMixin, _OrderedSetMixin, @@ -4155,9 +4155,9 @@ def test_indexed_set(self): self.assertTrue(m.I[1].isordered()) self.assertTrue(m.I[2].isordered()) self.assertTrue(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _InsertionOrderSetData) - self.assertIs(type(m.I[2]), _InsertionOrderSetData) - self.assertIs(type(m.I[3]), _InsertionOrderSetData) + self.assertIs(type(m.I[1]), InsertionOrderSetData) + self.assertIs(type(m.I[2]), InsertionOrderSetData) + self.assertIs(type(m.I[3]), InsertionOrderSetData) self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (4, 2, 5), 3: (4, 2, 5)}) # Explicit (constant) construction diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4f255e3567a..e6bd9d98a7d 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -127,7 +127,7 @@ def test_template_scalar_with_set(self): # Note that structural expressions do not implement polynomial_degree with self.assertRaisesRegex( AttributeError, - "'_InsertionOrderSetData' object has " "no attribute 'polynomial_degree'", + "'InsertionOrderSetData' object has " "no attribute 'polynomial_degree'", ): e.polynomial_degree() self.assertEqual(str(e), "s[{I}]") From d4d522a4e9b38b17f1944cdbab4292da2b11a220 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:49:48 -0600 Subject: [PATCH 0746/1178] Renamed _LogicalConstraintData -> LogicalConstraintData --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/logical_constraint.py | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 7003cc3d720..7d1bd1401f7 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -75,7 +75,7 @@ from pyomo.core.base.logical_constraint import ( LogicalConstraint, LogicalConstraintList, - _LogicalConstraintData, + LogicalConstraintData, ) from pyomo.core.base.objective import ( simple_objective_rule, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index faf6553be1b..341cd1506ff 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -803,7 +803,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, - # GeneralExpressionData, _LogicalConstraintData, + # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, # _ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 9af99c9ce5c..23a422705df 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -42,7 +42,7 @@ """ -class _LogicalConstraintData(ActiveComponentData): +class LogicalConstraintData(ActiveComponentData): """ This class defines the data for a single logical constraint. @@ -99,7 +99,12 @@ def get_value(self): raise NotImplementedError -class GeneralLogicalConstraintData(_LogicalConstraintData): +class _LogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = LogicalConstraintData + __renamed__version__ = '6.7.2.dev0' + + +class GeneralLogicalConstraintData(LogicalConstraintData): """ This class defines the data for a single general logical constraint. @@ -123,7 +128,7 @@ def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: - # - _LogicalConstraintData, + # - LogicalConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -455,7 +460,7 @@ def body(self): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # True are managed. But after that they will behave - # like _LogicalConstraintData objects where set_value expects + # like LogicalConstraintData objects where set_value expects # a valid expression or None. # From c37fc4fb13794b35e94e86b3a185494d2383f432 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:50:02 -0600 Subject: [PATCH 0747/1178] Renamed _ObjectiveData -> ObjectiveData --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/objective.py | 11 ++++++++--- pyomo/core/beta/dict_objects.py | 4 ++-- pyomo/core/beta/list_objects.py | 4 ++-- pyomo/core/plugins/transform/scaling.py | 4 ++-- pyomo/core/tests/unit/test_obj.py | 2 +- pyomo/core/tests/unit/test_suffix.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 6 +++--- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 7d1bd1401f7..408cf16c00e 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -82,7 +82,7 @@ simple_objectivelist_rule, Objective, ObjectiveList, - _ObjectiveData, + ObjectiveData, ) from pyomo.core.base.connector import Connector from pyomo.core.base.sos import SOSConstraint diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index b89214377ab..58cb198e1ae 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -86,7 +86,7 @@ def O_rule(model, i, j): # -class _ObjectiveData(ExpressionData): +class ObjectiveData(ExpressionData): """ This class defines the data for a single objective. @@ -119,8 +119,13 @@ def set_sense(self, sense): raise NotImplementedError +class _ObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = ObjectiveData + __renamed__version__ = '6.7.2.dev0' + + class GeneralObjectiveData( - GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData + GeneralExpressionDataImpl, ObjectiveData, ActiveComponentData ): """ This class defines the data for a single objective. @@ -479,7 +484,7 @@ def sense(self, sense): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Objective.Skip are managed. But after that they will behave - # like _ObjectiveData objects where set_value does not handle + # like ObjectiveData objects where set_value does not handle # Objective.Skip but expects a valid expression or None # diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index 2b23d81e91a..7c44166f189 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -16,7 +16,7 @@ from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData -from pyomo.core.base.objective import IndexedObjective, _ObjectiveData +from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableMapping @@ -202,7 +202,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ObjectiveData, *args, **kwds) + ComponentDict.__init__(self, ObjectiveData, *args, **kwds) class ExpressionDict(ComponentDict, IndexedExpression): diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index dd199eb70cd..d10a30e18e2 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -16,7 +16,7 @@ from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData -from pyomo.core.base.objective import IndexedObjective, _ObjectiveData +from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableSequence @@ -250,7 +250,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ObjectiveData, *args, **kwds) + ComponentList.__init__(self, ObjectiveData, *args, **kwds) class XExpressionList(ComponentList, IndexedExpression): diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 6b83a2378d1..ef418f094ae 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -16,7 +16,7 @@ Constraint, Objective, ConstraintData, - _ObjectiveData, + ObjectiveData, Suffix, value, ) @@ -226,7 +226,7 @@ def _apply_to(self, model, rename=True): else: c.set_value((lower, body, upper)) - elif isinstance(c, _ObjectiveData): + elif isinstance(c, ObjectiveData): c.expr = scaling_factor * replace_expressions( expr=c.expr, substitution_map=variable_substitution_dict, diff --git a/pyomo/core/tests/unit/test_obj.py b/pyomo/core/tests/unit/test_obj.py index 3c8a05f7058..dc2e320e63b 100644 --- a/pyomo/core/tests/unit/test_obj.py +++ b/pyomo/core/tests/unit/test_obj.py @@ -78,7 +78,7 @@ def test_empty_singleton(self): # Even though we construct a ScalarObjective, # if it is not initialized that means it is "empty" # and we should encounter errors when trying to access the - # _ObjectiveData interface methods until we assign + # ObjectiveData interface methods until we assign # something to the objective. # self.assertEqual(a._constructed, True) diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 9597bad7571..70f028a3eff 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1567,7 +1567,7 @@ def test_clone_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_clone_ObjectiveData(self): + def test_cloneObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=lambda model, i: model.x[i]) @@ -1725,7 +1725,7 @@ def test_pickle_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_pickle_ObjectiveData(self): + def test_pickleObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=simple_obj_rule) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e1afb5720f3..c010cee5e54 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -74,7 +74,7 @@ from pyomo.core.base.objective import ( ScalarObjective, GeneralObjectiveData, - _ObjectiveData, + ObjectiveData, ) from pyomo.core.base.suffix import SuffixFinder from pyomo.core.base.var import _VarData @@ -139,7 +139,7 @@ class NLWriterInfo(object): The list of (active) Pyomo model constraints in the order written to the NL file - objectives: List[_ObjectiveData] + objectives: List[ObjectiveData] The list of (active) Pyomo model objectives in the order written to the NL file @@ -466,7 +466,7 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (_VarData, ConstraintData, _ObjectiveData)): + elif isinstance(obj, (_VarData, ConstraintData, ObjectiveData)): missing_component_data.add(obj) elif isinstance(obj, (Var, Constraint, Objective)): # Expand this indexed component to store the From 4127432baf064f55d2f09d7c264109734e70f4b7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:50:05 -0600 Subject: [PATCH 0748/1178] Renamed _OrderedSetData -> OrderedSetData --- pyomo/core/base/set.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c0e9491ed1c..c7a7edc8e4b 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1301,7 +1301,7 @@ class FiniteSetData(_FiniteSetMixin, _SetData): def __init__(self, component): _SetData.__init__(self, component=component) - # Derived classes (like _OrderedSetData) may want to change the + # Derived classes (like OrderedSetData) may want to change the # storage if not hasattr(self, '_values'): self._values = set() @@ -1635,7 +1635,7 @@ def _to_0_based_index(self, item): ) -class _OrderedSetData(_OrderedSetMixin, FiniteSetData): +class OrderedSetData(_OrderedSetMixin, FiniteSetData): """ This class defines the base class for an ordered set of concrete data. @@ -1735,7 +1735,12 @@ def ord(self, item): raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name)) -class InsertionOrderSetData(_OrderedSetData): +class _OrderedSetData(metaclass=RenamedClass): + __renamed__new_class__ = OrderedSetData + __renamed__version__ = '6.7.2.dev0' + + +class InsertionOrderSetData(OrderedSetData): """ This class defines the data for a ordered set where the items are ordered in insertion order (similar to Python's OrderedSet. @@ -1786,7 +1791,7 @@ def sorted_iter(self): return iter(self) -class _SortedSetData(_SortedSetMixin, _OrderedSetData): +class _SortedSetData(_SortedSetMixin, OrderedSetData): """ This class defines the data for a sorted set. @@ -1801,7 +1806,7 @@ class _SortedSetData(_SortedSetMixin, _OrderedSetData): def __init__(self, component): # An empty set is sorted... self._is_sorted = True - _OrderedSetData.__init__(self, component=component) + OrderedSetData.__init__(self, component=component) def _iter_impl(self): """ From ec3f121f81a4f52295caab029d5bfb5e826c569e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:51:57 -0600 Subject: [PATCH 0749/1178] Renamed _ParamData -> ParamData --- pyomo/contrib/appsi/base.py | 14 ++++---- pyomo/contrib/appsi/fbbt.py | 6 ++-- pyomo/contrib/appsi/solvers/cbc.py | 6 ++-- pyomo/contrib/appsi/solvers/cplex.py | 6 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 6 ++-- pyomo/contrib/appsi/solvers/highs.py | 6 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 6 ++-- pyomo/contrib/appsi/solvers/wntr.py | 6 ++-- pyomo/contrib/appsi/writers/lp_writer.py | 6 ++-- pyomo/contrib/appsi/writers/nl_writer.py | 6 ++-- pyomo/contrib/cp/repn/docplex_writer.py | 4 +-- .../logical_to_disjunctive_walker.py | 4 +-- pyomo/contrib/latex_printer/latex_printer.py | 12 +++---- pyomo/contrib/pyros/config.py | 8 ++--- pyomo/contrib/pyros/tests/test_config.py | 8 ++--- pyomo/contrib/solver/base.py | 6 ++-- pyomo/contrib/solver/gurobi.py | 6 ++-- pyomo/contrib/solver/persistent.py | 10 +++--- pyomo/contrib/viewer/model_browser.py | 8 ++--- pyomo/core/base/component.py | 2 +- pyomo/core/base/param.py | 35 +++++++++++-------- pyomo/core/expr/calculus/derivatives.py | 2 +- pyomo/core/tests/unit/test_param.py | 10 +++--- pyomo/core/tests/unit/test_visitor.py | 4 +-- pyomo/repn/plugins/ampl/ampl_.py | 8 ++--- pyomo/repn/standard_repn.py | 6 ++-- 26 files changed, 102 insertions(+), 99 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 1ce24220bfd..e50d5201090 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -24,7 +24,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData, Var -from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap @@ -803,7 +803,7 @@ def add_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): pass @abc.abstractmethod @@ -819,7 +819,7 @@ def remove_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): pass @abc.abstractmethod @@ -975,10 +975,10 @@ def add_variables(self, variables: List[GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): for p in params: self._params[id(p)] = p self._add_params(params) @@ -1198,10 +1198,10 @@ def remove_variables(self, variables: List[GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._remove_params(params) for p in params: del self._params[id(p)] diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index a360d2bce84..7735318f8ba 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -19,7 +19,7 @@ from .cmodel import cmodel, cmodel_available from typing import List, Optional from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize @@ -143,7 +143,7 @@ def _add_variables(self, variables: List[GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -198,7 +198,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): cvar = self._var_map.pop(id(v)) del self._rvar_map[cvar] - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): if self._symbolic_solver_labels: for p in params: self._symbol_map.removeSymbol(p) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index cd5158d905e..d03e6e31c54 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -29,7 +29,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream @@ -167,7 +167,7 @@ def set_instance(self, model): def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) def add_constraints(self, cons: List[GeneralConstraintData]): @@ -179,7 +179,7 @@ def add_block(self, block: BlockData): def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) def remove_constraints(self, cons: List[GeneralConstraintData]): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index cdd699105be..55259244d45 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -25,7 +25,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer import sys @@ -182,7 +182,7 @@ def set_instance(self, model): def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) def add_constraints(self, cons: List[GeneralConstraintData]): @@ -194,7 +194,7 @@ def add_block(self, block: BlockData): def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) def remove_constraints(self, cons: List[GeneralConstraintData]): diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 6da59042a80..4392cdf0839 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -26,7 +26,7 @@ from pyomo.core.base.var import Var, GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -489,7 +489,7 @@ def _add_variables(self, variables: List[GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass def _reinit(self): @@ -771,7 +771,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass def _update_variables(self, variables: List[GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index ded0092f38b..a6b7c102c91 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -23,7 +23,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -335,7 +335,7 @@ def _add_variables(self, variables: List[GeneralVarData]): len(vtypes), np.array(indices), np.array(vtypes) ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass def _reinit(self): @@ -515,7 +515,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): self._pyomo_var_to_solver_var_map.clear() self._pyomo_var_to_solver_var_map.update(new_var_map) - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass def _update_variables(self, variables: List[GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 9ccb58095b1..ca75a1b02c8 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -31,7 +31,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream @@ -231,7 +231,7 @@ def set_instance(self, model): def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) def add_constraints(self, cons: List[GeneralConstraintData]): @@ -243,7 +243,7 @@ def add_block(self, block: BlockData): def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) def remove_constraints(self, cons: List[GeneralConstraintData]): diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 7f633161fe1..8f2650dabb6 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -41,7 +41,7 @@ from typing import Dict, Optional, List from pyomo.core.base.block import BlockData from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler @@ -270,7 +270,7 @@ def _add_variables(self, variables: List[GeneralVarData]): ) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): aml = wntr.sim.aml.aml for p in params: pname = self._symbol_map.getSymbol(p, self._labeler) @@ -314,7 +314,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): del self._solver_model._wntr_fixed_var_cons[v_id] self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): for p in params: p_id = id(p) solver_param = self._pyomo_param_to_solver_param_map[p_id] diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 94af5ba7e93..3a168cdcd91 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from typing import List -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData @@ -91,7 +91,7 @@ def _add_variables(self, variables: List[GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -123,7 +123,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): del self._solver_var_to_pyomo_var_map[cvar] self._symbol_map.removeSymbol(v) - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index b7dab1d5a3e..fced3c5ae10 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from typing import List -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData @@ -100,7 +100,7 @@ def _add_variables(self, variables: List[GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -153,7 +153,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): if self.config.symbolic_solver_labels: for p in params: self._symbol_map.removeSymbol(p) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index eb50a543160..75095755895 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -64,7 +64,7 @@ IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData +from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables @@ -970,7 +970,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, - _ParamData: _before_param, + ParamData: _before_param, } def __init__(self, cpx_model, symbolic_solver_labels=False): diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 26b63d020a5..a228b1561dd 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -28,7 +28,7 @@ ) import pyomo.core.base.boolean_var as BV from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.param import ScalarParam, _ParamData +from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, GeneralVarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -211,7 +211,7 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var _before_child_dispatcher[BV.GeneralBooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[_ParamData] = _dispatch_param +_before_child_dispatcher[ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param # for the moment, these are all just so we can get good error messages when we # don't handle them: diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index efcd3016dbf..dec058bb5ba 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -48,7 +48,7 @@ templatize_rule, ) from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar -from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam +from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam from pyomo.core.base.set import _SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint from pyomo.common.collections.component_map import ComponentMap @@ -417,7 +417,7 @@ def __init__(self): Numeric_GetItemExpression: handle_numericGetItemExpression_node, TemplateSumExpression: handle_templateSumExpression_node, ScalarParam: handle_param_node, - _ParamData: handle_param_node, + ParamData: handle_param_node, IndexedParam: handle_param_node, NPV_Numeric_GetItemExpression: handle_numericGetItemExpression_node, IndexedBlock: handle_indexedBlock_node, @@ -717,10 +717,8 @@ def latex_printer( variableList.append(v) parameterList = [] - for p in identify_components( - temp_comp, [ScalarParam, _ParamData, IndexedParam] - ): - if isinstance(p, _ParamData): + for p in identify_components(temp_comp, [ScalarParam, ParamData, IndexedParam]): + if isinstance(p, ParamData): p_write = p.parent_component() if p_write not in ComponentSet(parameterList): parameterList.append(p_write) @@ -1280,7 +1278,7 @@ def get_index_names(st, lcm): if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') rep_dict[variableMap[ky]] = overwrite_value - elif isinstance(ky, (pyo.Param, _ParamData)): + elif isinstance(ky, (pyo.Param, ParamData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index bc2bfd591e6..e60b474d037 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -17,7 +17,7 @@ ) from pyomo.common.errors import ApplicationError, PyomoException from pyomo.core.base import Var, _VarData -from pyomo.core.base.param import Param, _ParamData +from pyomo.core.base.param import Param, ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet @@ -62,7 +62,7 @@ def mutable_param_validator(param_obj): Parameters ---------- - param_obj : Param or _ParamData + param_obj : Param or ParamData Param-like object of interest. Raises @@ -98,7 +98,7 @@ class InputDataStandardizer(object): Pyomo component type, such as Component, Var or Param. cdatatype : type Corresponding Pyomo component data type, such as - _ComponentData, _VarData, or _ParamData. + _ComponentData, _VarData, or ParamData. ctype_validator : callable, optional Validator function for objects of type `ctype`. cdatatype_validator : callable, optional @@ -531,7 +531,7 @@ def pyros_config(): default=[], domain=InputDataStandardizer( ctype=Param, - cdatatype=_ParamData, + cdatatype=ParamData, ctype_validator=mutable_param_validator, allow_repeats=False, ), diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 0f52d04135d..cd635e795fc 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -8,7 +8,7 @@ from pyomo.core.base import ConcreteModel, Var, _VarData from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError -from pyomo.core.base.param import Param, _ParamData +from pyomo.core.base.param import Param, ParamData from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, @@ -201,7 +201,7 @@ def test_standardizer_invalid_uninitialized_params(self): uninitialized entries passed. """ standardizer_func = InputDataStandardizer( - ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator ) mdl = ConcreteModel() @@ -217,7 +217,7 @@ def test_standardizer_invalid_immutable_params(self): Param object(s) passed. """ standardizer_func = InputDataStandardizer( - ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator ) mdl = ConcreteModel() @@ -237,7 +237,7 @@ def test_standardizer_valid_mutable_params(self): mdl.p2 = Param(["a", "b"], initialize=1, mutable=True) standardizer_func = InputDataStandardizer( - ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator ) standardizer_input = [mdl.p1[0], mdl.p2] diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index a935a950819..1b22c17cf48 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -16,7 +16,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue @@ -288,7 +288,7 @@ def add_variables(self, variables: List[GeneralVarData]): """ @abc.abstractmethod - def add_parameters(self, params: List[_ParamData]): + def add_parameters(self, params: List[ParamData]): """ Add parameters to the model """ @@ -312,7 +312,7 @@ def remove_variables(self, variables: List[GeneralVarData]): """ @abc.abstractmethod - def remove_parameters(self, params: List[_ParamData]): + def remove_parameters(self, params: List[ParamData]): """ Remove parameters from the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 353798133db..107de15e625 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -25,7 +25,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -469,7 +469,7 @@ def _add_variables(self, variables: List[GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_parameters(self, params: List[_ParamData]): + def _add_parameters(self, params: List[ParamData]): pass def _reinit(self): @@ -747,7 +747,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_parameters(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[ParamData]): pass def _update_variables(self, variables: List[GeneralVarData]): diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index aeacc9f87c4..558b8cbf314 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -15,7 +15,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.param import ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer @@ -75,10 +75,10 @@ def add_variables(self, variables: List[GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_parameters(self, params: List[_ParamData]): + def _add_parameters(self, params: List[ParamData]): pass - def add_parameters(self, params: List[_ParamData]): + def add_parameters(self, params: List[ParamData]): for p in params: self._params[id(p)] = p self._add_parameters(params) @@ -274,10 +274,10 @@ def remove_variables(self, variables: List[GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_parameters(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[ParamData]): pass - def remove_parameters(self, params: List[_ParamData]): + def remove_parameters(self, params: List[ParamData]): self._remove_parameters(params) for p in params: del self._params[id(p)] diff --git a/pyomo/contrib/viewer/model_browser.py b/pyomo/contrib/viewer/model_browser.py index 5887a577ba0..91dc946c55d 100644 --- a/pyomo/contrib/viewer/model_browser.py +++ b/pyomo/contrib/viewer/model_browser.py @@ -33,7 +33,7 @@ import pyomo.contrib.viewer.qt as myqt from pyomo.contrib.viewer.report import value_no_exception, get_residual -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.environ import ( Block, BooleanVar, @@ -243,7 +243,7 @@ def _get_expr_callback(self): return None def _get_value_callback(self): - if isinstance(self.data, _ParamData): + if isinstance(self.data, ParamData): v = value_no_exception(self.data, div0="divide_by_0") # Check the param value for numpy float and int, sometimes numpy # values can sneak in especially if you set parameters from data @@ -295,7 +295,7 @@ def _get_residual_callback(self): def _get_units_callback(self): if isinstance(self.data, (Var, Var._ComponentDataClass)): return str(units.get_units(self.data)) - if isinstance(self.data, (Param, _ParamData)): + if isinstance(self.data, (Param, ParamData)): return str(units.get_units(self.data)) return self._cache_units @@ -320,7 +320,7 @@ def _set_value_callback(self, val): o.value = val except: return - elif isinstance(self.data, _ParamData): + elif isinstance(self.data, ParamData): if not self.data.parent_component().mutable: return try: diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 341cd1506ff..33b2f5c686c 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # _ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, + # ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 5fcaf92b25a..9af6a37de45 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -118,7 +118,7 @@ def _parent(self, val): pass -class _ParamData(ComponentData, NumericValue): +class ParamData(ComponentData, NumericValue): """ This class defines the data for a mutable parameter. @@ -252,6 +252,11 @@ def _compute_polynomial_degree(self, result): return 0 +class _ParamData(metaclass=RenamedClass): + __renamed__new_class__ = ParamData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "Parameter data that is used to define a model instance." ) @@ -285,7 +290,7 @@ class Param(IndexedComponent, IndexedComponent_NDArrayMixin): """ DefaultMutable = False - _ComponentDataClass = _ParamData + _ComponentDataClass = ParamData class NoValue(object): """A dummy type that is pickle-safe that we can use as the default @@ -523,14 +528,14 @@ def store_values(self, new_values, check=True): # instead of incurring the penalty of checking. for index, new_value in new_values.items(): if index not in self._data: - self._data[index] = _ParamData(self) + self._data[index] = ParamData(self) self._data[index]._value = new_value else: # For scalars, we will choose an approach based on # how "dense" the Param is if not self._data: # empty for index in self._index_set: - p = self._data[index] = _ParamData(self) + p = self._data[index] = ParamData(self) p._value = new_values elif len(self._data) == len(self._index_set): for index in self._index_set: @@ -538,7 +543,7 @@ def store_values(self, new_values, check=True): else: for index in self._index_set: if index not in self._data: - self._data[index] = _ParamData(self) + self._data[index] = ParamData(self) self._data[index]._value = new_values else: # @@ -601,9 +606,9 @@ def _getitem_when_not_present(self, index): # a default value, as long as *solving* a model without # reasonable values produces an informative error. if self._mutable: - # Note: _ParamData defaults to Param.NoValue + # Note: ParamData defaults to Param.NoValue if self.is_indexed(): - ans = self._data[index] = _ParamData(self) + ans = self._data[index] = ParamData(self) else: ans = self._data[index] = self ans._index = index @@ -698,8 +703,8 @@ def _setitem_impl(self, index, obj, value): return obj else: old_value, self._data[index] = self._data[index], value - # Because we do not have a _ParamData, we cannot rely on the - # validation that occurs in _ParamData.set_value() + # Because we do not have a ParamData, we cannot rely on the + # validation that occurs in ParamData.set_value() try: self._validate_value(index, value) return value @@ -736,14 +741,14 @@ def _setitem_when_not_present(self, index, value, _check_domain=True): self._index = UnindexedComponent_index return self elif self._mutable: - obj = self._data[index] = _ParamData(self) + obj = self._data[index] = ParamData(self) obj.set_value(value, index) obj._index = index return obj else: self._data[index] = value - # Because we do not have a _ParamData, we cannot rely on the - # validation that occurs in _ParamData.set_value() + # Because we do not have a ParamData, we cannot rely on the + # validation that occurs in ParamData.set_value() self._validate_value(index, value, _check_domain) return value except: @@ -901,9 +906,9 @@ def _pprint(self): return (headers, self.sparse_iteritems(), ("Value",), dataGen) -class ScalarParam(_ParamData, Param): +class ScalarParam(ParamData, Param): def __init__(self, *args, **kwds): - _ParamData.__init__(self, component=self) + ParamData.__init__(self, component=self) Param.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -996,7 +1001,7 @@ def _create_objects_for_deepcopy(self, memo, component_list): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args) -> _ParamData: + def __getitem__(self, args) -> ParamData: try: return super().__getitem__(args) except: diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index cd23cb16b2c..5df1fd3c65e 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -42,7 +42,7 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): wrt: pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with respect to wrt. wrt is normally a GeneralVarData, but could - also be a _ParamData. wrt and wrt_list cannot both be specified. + also be a ParamData. wrt and wrt_list cannot both be specified. wrt_list: list of pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index 9bc0c4b2ad2..b39272879f6 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -65,7 +65,7 @@ from pyomo.common.errors import PyomoException from pyomo.common.log import LoggingIntercept from pyomo.common.tempfiles import TempfileManager -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.set import _SetData from pyomo.core.base.units_container import units, pint_available, UnitsError @@ -181,7 +181,7 @@ def test_setitem_preexisting(self): idx = sorted(keys)[0] self.assertEqual(value(self.instance.A[idx]), self.data[idx]) if self.instance.A.mutable: - self.assertTrue(isinstance(self.instance.A[idx], _ParamData)) + self.assertTrue(isinstance(self.instance.A[idx], ParamData)) else: self.assertEqual(type(self.instance.A[idx]), float) @@ -190,7 +190,7 @@ def test_setitem_preexisting(self): if not self.instance.A.mutable: self.fail("Expected setitem[%s] to fail for immutable Params" % (idx,)) self.assertEqual(value(self.instance.A[idx]), 4.3) - self.assertTrue(isinstance(self.instance.A[idx], _ParamData)) + self.assertTrue(isinstance(self.instance.A[idx], ParamData)) except TypeError: # immutable Params should raise a TypeError exception if self.instance.A.mutable: @@ -249,7 +249,7 @@ def test_setitem_default_override(self): self.assertEqual(value(self.instance.A[idx]), self.instance.A._default_val) if self.instance.A.mutable: - self.assertIsInstance(self.instance.A[idx], _ParamData) + self.assertIsInstance(self.instance.A[idx], ParamData) else: self.assertEqual( type(self.instance.A[idx]), type(value(self.instance.A._default_val)) @@ -260,7 +260,7 @@ def test_setitem_default_override(self): if not self.instance.A.mutable: self.fail("Expected setitem[%s] to fail for immutable Params" % (idx,)) self.assertEqual(self.instance.A[idx].value, 4.3) - self.assertIsInstance(self.instance.A[idx], _ParamData) + self.assertIsInstance(self.instance.A[idx], ParamData) except TypeError: # immutable Params should raise a TypeError exception if self.instance.A.mutable: diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 12fb98d1d19..ac61a3a24c7 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -72,7 +72,7 @@ RECURSION_LIMIT, get_stack_depth, ) -from pyomo.core.base.param import _ParamData, ScalarParam +from pyomo.core.base.param import ParamData, ScalarParam from pyomo.core.expr.template_expr import IndexTemplate from pyomo.common.collections import ComponentSet from pyomo.common.errors import TemplateExpressionError @@ -685,7 +685,7 @@ def __init__(self, model): self.model = model def visiting_potential_leaf(self, node): - if node.__class__ in (_ParamData, ScalarParam): + if node.__class__ in (ParamData, ScalarParam): if id(node) in self.substitute: return True, self.substitute[id(node)] self.substitute[id(node)] = 2 * self.model.w.add() diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index c6357cbecd9..840bee2166c 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -171,8 +171,8 @@ def _build_op_template(): _op_template[var._VarData] = "v%d{C}\n" _op_comment[var._VarData] = "\t#%s" - _op_template[param._ParamData] = "n%r{C}\n" - _op_comment[param._ParamData] = "" + _op_template[param.ParamData] = "n%r{C}\n" + _op_comment[param.ParamData] = "" _op_template[NumericConstant] = "n%r{C}\n" _op_comment[NumericConstant] = "" @@ -749,8 +749,8 @@ def _print_nonlinear_terms_NL(self, exp): ) ) - elif isinstance(exp, param._ParamData): - OUTPUT.write(self._op_string[param._ParamData] % (value(exp))) + elif isinstance(exp, param.ParamData): + OUTPUT.write(self._op_string[param.ParamData] % (value(exp))) elif isinstance(exp, NumericConstant) or exp.is_fixed(): OUTPUT.write(self._op_string[NumericConstant] % (value(exp))) diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 907b4a2b115..5786d078385 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -23,7 +23,7 @@ from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, GeneralVarData, value -from pyomo.core.base.param import ScalarParam, _ParamData +from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable from pyomo.core.kernel.objective import objective @@ -1138,7 +1138,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra EXPR.ExternalFunctionExpression: _collect_external_fn, # ConnectorData : _collect_linear_connector, # ScalarConnector : _collect_linear_connector, - _ParamData: _collect_const, + ParamData: _collect_const, ScalarParam: _collect_const, # param.Param : _collect_linear_const, # parameter : _collect_linear_const, @@ -1538,7 +1538,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ##EXPR.LinearSumExpression : _collect_linear_sum, ##ConnectorData : _collect_linear_connector, ##ScalarConnector : _collect_linear_connector, - ##param._ParamData : _collect_linear_const, + ##param.ParamData : _collect_linear_const, ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, From 8c968e3e976bc8ea8c551a7c48d275d623e04559 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:51:57 -0600 Subject: [PATCH 0750/1178] Renamed _PiecewiseData -> PiecewiseData --- pyomo/core/base/piecewise.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index b15def13ccb..43f8ddbfef5 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -214,7 +214,7 @@ def _characterize_function(name, tol, f_rule, model, points, *index): return 0, values, False -class _PiecewiseData(BlockData): +class PiecewiseData(BlockData): """ This class defines the base class for all linearization and piecewise constraint generators.. @@ -272,6 +272,11 @@ def __call__(self, x): ) +class _PiecewiseData(metaclass=RenamedClass): + __renamed__new_class__ = PiecewiseData + __renamed__version__ = '6.7.2.dev0' + + class _SimpleSinglePiecewise(object): """ Called when the piecewise points list has only two points @@ -1125,7 +1130,7 @@ def f(model,j,x): not be modified. """ - _ComponentDataClass = _PiecewiseData + _ComponentDataClass = PiecewiseData def __new__(cls, *args, **kwds): if cls != Piecewise: @@ -1541,7 +1546,7 @@ def add(self, index, _is_indexed=None): raise ValueError(msg % (self.name, index, self._pw_rep)) if _is_indexed: - comp = _PiecewiseData(self) + comp = PiecewiseData(self) else: comp = self self._data[index] = comp @@ -1551,9 +1556,9 @@ def add(self, index, _is_indexed=None): comp.build_constraints(func, _self_xvar, _self_yvar) -class SimplePiecewise(_PiecewiseData, Piecewise): +class SimplePiecewise(PiecewiseData, Piecewise): def __init__(self, *args, **kwds): - _PiecewiseData.__init__(self, self) + PiecewiseData.__init__(self, self) Piecewise.__init__(self, *args, **kwds) From 590e21fad08496e01fc6611fe506867b2ae6217d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:52:11 -0600 Subject: [PATCH 0751/1178] Renamed _PortData -> PortData --- pyomo/core/base/component.py | 2 +- pyomo/network/port.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 33b2f5c686c..7a4b7e40aab 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -806,7 +806,7 @@ class ComponentData(_ComponentBase): # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, # ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, - # ArcData, _PortData, _LinearConstraintData, and + # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! def __init__(self, component): diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 26822d4fee9..ee5c915d8db 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -36,7 +36,7 @@ logger = logging.getLogger('pyomo.network') -class _PortData(ComponentData): +class PortData(ComponentData): """ This class defines the data for a single Port @@ -285,6 +285,11 @@ def get_split_fraction(self, arc): return res +class _PortData(metaclass=RenamedClass): + __renamed__new_class__ = PortData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A bundle of variables that can be connected to other ports." ) @@ -339,7 +344,7 @@ def __init__(self, *args, **kwd): # IndexedComponent that support implicit definition def _getitem_when_not_present(self, idx): """Returns the default component data value.""" - tmp = self._data[idx] = _PortData(component=self) + tmp = self._data[idx] = PortData(component=self) tmp._index = idx return tmp @@ -357,7 +362,7 @@ def construct(self, data=None): for _set in self._anonymous_sets: _set.construct() - # Construct _PortData objects for all index values + # Construct PortData objects for all index values if self.is_indexed(): self._initialize_members(self._index_set) else: @@ -763,9 +768,9 @@ def _create_evar(member, name, eblock, index_set): return evar -class ScalarPort(Port, _PortData): +class ScalarPort(Port, PortData): def __init__(self, *args, **kwd): - _PortData.__init__(self, component=self) + PortData.__init__(self, component=self) Port.__init__(self, *args, **kwd) self._index = UnindexedComponent_index From 199ee006d445859f7d796f528a8b326cf883f3f6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:55:57 -0600 Subject: [PATCH 0752/1178] Renamed _SetData -> SetData --- pyomo/contrib/latex_printer/latex_printer.py | 4 +- pyomo/core/base/set.py | 51 +++++++++++--------- pyomo/core/base/sets.py | 4 +- pyomo/core/tests/unit/test_param.py | 4 +- pyomo/core/tests/unit/test_set.py | 18 +++---- 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index dec058bb5ba..28d1ca52943 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -49,7 +49,7 @@ ) from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam -from pyomo.core.base.set import _SetData, SetOperator +from pyomo.core.base.set import SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint from pyomo.common.collections.component_map import ComponentMap from pyomo.common.collections.component_set import ComponentSet @@ -1283,7 +1283,7 @@ def get_index_names(st, lcm): if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') rep_dict[parameterMap[ky]] = overwrite_value - elif isinstance(ky, _SetData): + elif isinstance(ky, SetData): # already handled pass elif isinstance(ky, (float, int)): diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c7a7edc8e4b..3280f512e83 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -87,7 +87,7 @@ 0. `class _SetDataBase(ComponentData)` *(pure virtual interface)* -1. `class _SetData(_SetDataBase)` +1. `class SetData(_SetDataBase)` *(base class for all AML Sets)* 2. `class _FiniteSetMixin(object)` @@ -102,7 +102,7 @@ bounded continuous ranges as well as unbounded discrete ranges). As there are an infinite number of values, iteration is *not* supported. The base class also implements all Python set operations. -Note that `_SetData` does *not* implement `len()`, as Python requires +Note that `SetData` does *not* implement `len()`, as Python requires `len()` to return a positive integer. Finite sets add iteration and support for `len()`. In addition, they @@ -520,7 +520,7 @@ class _SetDataBase(ComponentData): __slots__ = () -class _SetData(_SetDataBase): +class SetData(_SetDataBase): """The base for all Pyomo AML objects that can be used as a component indexing set. @@ -534,13 +534,13 @@ def __contains__(self, value): ans = self.get(value, _NotFound) except TypeError: # In Python 3.x, Sets are unhashable - if isinstance(value, _SetData): + if isinstance(value, SetData): ans = _NotFound else: raise if ans is _NotFound: - if isinstance(value, _SetData): + if isinstance(value, SetData): deprecation_warning( "Testing for set subsets with 'a in b' is deprecated. " "Use 'a.issubset(b)'.", @@ -1188,6 +1188,11 @@ def __gt__(self, other): return self >= other and not self == other +class _SetData(metaclass=RenamedClass): + __renamed__new_class__ = SetData + __renamed__version__ = '6.7.2.dev0' + + class _FiniteSetMixin(object): __slots__ = () @@ -1294,13 +1299,13 @@ def ranges(self): yield NonNumericRange(i) -class FiniteSetData(_FiniteSetMixin, _SetData): +class FiniteSetData(_FiniteSetMixin, SetData): """A general unordered iterable Set""" __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') def __init__(self, component): - _SetData.__init__(self, component=component) + SetData.__init__(self, component=component) # Derived classes (like OrderedSetData) may want to change the # storage if not hasattr(self, '_values'): @@ -1986,7 +1991,7 @@ class SortedOrder(object): _UnorderedInitializers = {set} @overload - def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: ... + def __new__(cls: Type[Set], *args, **kwds) -> Union[SetData, IndexedSet]: ... @overload def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... @@ -2193,7 +2198,7 @@ def _getitem_when_not_present(self, index): """Returns the default component data value.""" # Because we allow sets within an IndexedSet to have different # dimen, we have moved the tuplization logic from PyomoModel - # into Set (because we cannot know the dimen of a _SetData until + # into Set (because we cannot know the dimen of a SetData until # we are actually constructing that index). This also means # that we need to potentially communicate the dimen to the # (wrapped) value initializer. So, we will get the dimen first, @@ -2353,7 +2358,7 @@ def _pprint(self): # else: # return '{' + str(ans)[1:-1] + "}" - # TBD: In the current design, we force all _SetData within an + # TBD: In the current design, we force all SetData within an # indexed Set to have the same isordered value, so we will only # print it once in the header. Is this a good design? try: @@ -2398,7 +2403,7 @@ def data(self): return {k: v.data() for k, v in self.items()} @overload - def __getitem__(self, index) -> _SetData: ... + def __getitem__(self, index) -> SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore @@ -2479,14 +2484,14 @@ class AbstractSortedSimpleSet(metaclass=RenamedClass): ############################################################################ -class SetOf(_SetData, Component): +class SetOf(SetData, Component): """""" def __new__(cls, *args, **kwds): if cls is not SetOf: return super(SetOf, cls).__new__(cls) (reference,) = args - if isinstance(reference, (_SetData, GlobalSetBase)): + if isinstance(reference, (SetData, GlobalSetBase)): if reference.isfinite(): if reference.isordered(): return super(SetOf, cls).__new__(OrderedSetOf) @@ -2500,7 +2505,7 @@ def __new__(cls, *args, **kwds): return super(SetOf, cls).__new__(FiniteSetOf) def __init__(self, reference, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) kwds.setdefault('ctype', SetOf) Component.__init__(self, **kwds) self._ref = reference @@ -2523,7 +2528,7 @@ def construct(self, data=None): @property def dimen(self): - if isinstance(self._ref, _SetData): + if isinstance(self._ref, SetData): return self._ref.dimen _iter = iter(self) try: @@ -2618,7 +2623,7 @@ def ord(self, item): ############################################################################ -class InfiniteRangeSetData(_SetData): +class InfiniteRangeSetData(SetData): """Data class for a infinite set. This Set implements an interface to an *infinite set* defined by one @@ -2630,7 +2635,7 @@ class InfiniteRangeSetData(_SetData): __slots__ = ('_ranges',) def __init__(self, component): - _SetData.__init__(self, component=component) + SetData.__init__(self, component=component) self._ranges = None def get(self, value, default=None): @@ -3298,11 +3303,11 @@ class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): ############################################################################ -class SetOperator(_SetData, Set): +class SetOperator(SetData, Set): __slots__ = ('_sets',) def __init__(self, *args, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) Set.__init__(self, **kwds) self._sets, _anonymous = zip(*(process_setarg(_set) for _set in args)) _anonymous = tuple(filter(None, _anonymous)) @@ -4242,9 +4247,9 @@ def ord(self, item): ############################################################################ -class _AnySet(_SetData, Set): +class _AnySet(SetData, Set): def __init__(self, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) # There is a chicken-and-egg game here: the SetInitializer uses # Any as part of the processing of the domain/within/bounds # domain restrictions. However, Any has not been declared when @@ -4298,9 +4303,9 @@ def get(self, val, default=None): return super(_AnyWithNoneSet, self).get(val, default) -class _EmptySet(_FiniteSetMixin, _SetData, Set): +class _EmptySet(_FiniteSetMixin, SetData, Set): def __init__(self, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) Set.__init__(self, **kwds) self.construct() diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index ca693cf7d8b..3ebdc6875d1 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -17,8 +17,8 @@ process_setarg, set_options, simple_set_rule, - _SetDataBase, - _SetData, + SetDataBase, + SetData, Set, SetOf, IndexedSet, diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index b39272879f6..f22674b6bf7 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -66,7 +66,7 @@ from pyomo.common.log import LoggingIntercept from pyomo.common.tempfiles import TempfileManager from pyomo.core.base.param import ParamData -from pyomo.core.base.set import _SetData +from pyomo.core.base.set import SetData from pyomo.core.base.units_container import units, pint_available, UnitsError from io import StringIO @@ -1487,7 +1487,7 @@ def test_domain_set_initializer(self): m.I = Set(initialize=[1, 2, 3]) param_vals = {1: 1, 2: 1, 3: -1} m.p = Param(m.I, initialize=param_vals, domain={-1, 1}) - self.assertIsInstance(m.p.domain, _SetData) + self.assertIsInstance(m.p.domain, SetData) @unittest.skipUnless(pint_available, "units test requires pint module") def test_set_value_units(self): diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 38870d5213e..abd5a03c755 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -81,7 +81,7 @@ SetProduct_InfiniteSet, SetProduct_FiniteSet, SetProduct_OrderedSet, - _SetData, + SetData, FiniteSetData, InsertionOrderSetData, _SortedSetData, @@ -4300,7 +4300,7 @@ def _l_tri(model, i, j): # This tests a filter that matches the dimentionality of the # component. construct() needs to recognize that the filter is # returning a constant in construct() and re-assign it to be the - # _filter for each _SetData + # _filter for each SetData def _lt_3(model, i): self.assertIs(model, m) return i < 3 @@ -5297,15 +5297,15 @@ def test_no_normalize_index(self): class TestAbstractSetAPI(unittest.TestCase): - def test_SetData(self): + def testSetData(self): # This tests an anstract non-finite set API m = ConcreteModel() m.I = Set(initialize=[1]) - s = _SetData(m.I) + s = SetData(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -5395,7 +5395,7 @@ def test_SetData(self): def test_FiniteMixin(self): # This tests an anstract finite set API - class FiniteMixin(_FiniteSetMixin, _SetData): + class FiniteMixin(_FiniteSetMixin, SetData): pass m = ConcreteModel() @@ -5403,7 +5403,7 @@ class FiniteMixin(_FiniteSetMixin, _SetData): s = FiniteMixin(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -5520,7 +5520,7 @@ class FiniteMixin(_FiniteSetMixin, _SetData): def test_OrderedMixin(self): # This tests an anstract ordered set API - class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, _SetData): + class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, SetData): pass m = ConcreteModel() @@ -5528,7 +5528,7 @@ class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, _SetData): s = OrderedMixin(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): From 43c7c40b032a5d3ef8512d8a9d54969d52f9e45a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:56:14 -0600 Subject: [PATCH 0753/1178] Renamed _SortedSetData -> SortedSetData --- pyomo/core/base/set.py | 27 ++++++++++++++++----------- pyomo/core/tests/unit/test_set.py | 8 ++++---- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 3280f512e83..d94cc86cf7c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1649,7 +1649,7 @@ class OrderedSetData(_OrderedSetMixin, FiniteSetData): implements a set ordered by insertion order, we make the "official" InsertionOrderSetData an empty derivative class, so that - issubclass(_SortedSetData, InsertionOrderSetData) == False + issubclass(SortedSetData, InsertionOrderSetData) == False Constructor Arguments: component The Set object that owns this data. @@ -1796,7 +1796,7 @@ def sorted_iter(self): return iter(self) -class _SortedSetData(_SortedSetMixin, OrderedSetData): +class SortedSetData(_SortedSetMixin, OrderedSetData): """ This class defines the data for a sorted set. @@ -1819,12 +1819,12 @@ def _iter_impl(self): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self)._iter_impl() + return super(SortedSetData, self)._iter_impl() def __reversed__(self): if not self._is_sorted: self._sort() - return super(_SortedSetData, self).__reversed__() + return super(SortedSetData, self).__reversed__() def _add_impl(self, value): # Note that the sorted status has no bearing on insertion, @@ -1838,7 +1838,7 @@ def _add_impl(self, value): # def discard(self, val): def clear(self): - super(_SortedSetData, self).clear() + super(SortedSetData, self).clear() self._is_sorted = True def at(self, index): @@ -1850,7 +1850,7 @@ def at(self, index): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self).at(index) + return super(SortedSetData, self).at(index) def ord(self, item): """ @@ -1862,7 +1862,7 @@ def ord(self, item): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self).ord(item) + return super(SortedSetData, self).ord(item) def sorted_data(self): return self.data() @@ -1875,6 +1875,11 @@ def _sort(self): self._is_sorted = True +class _SortedSetData(metaclass=RenamedClass): + __renamed__new_class__ = SortedSetData + __renamed__version__ = '6.7.2.dev0' + + ############################################################################ _SET_API = (('__contains__', 'test membership in'), 'get', 'ranges', 'bounds') @@ -2005,7 +2010,7 @@ def __new__(cls, *args, **kwds): # Many things are easier by forcing it to be consistent across # the set (namely, the _ComponentDataClass is constant). # However, it is a bit off that 'ordered' it the only arg NOT - # processed by Initializer. We can mock up a _SortedSetData + # processed by Initializer. We can mock up a SortedSetData # sort function that preserves Insertion Order (lambda x: x), but # the unsorted is harder (it would effectively be insertion # order, but ordered() may not be deterministic based on how the @@ -2052,7 +2057,7 @@ def __new__(cls, *args, **kwds): if ordered is Set.InsertionOrder: newObj._ComponentDataClass = InsertionOrderSetData elif ordered is Set.SortedOrder: - newObj._ComponentDataClass = _SortedSetData + newObj._ComponentDataClass = SortedSetData else: newObj._ComponentDataClass = FiniteSetData return newObj @@ -2435,13 +2440,13 @@ class OrderedSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class SortedScalarSet(_ScalarOrderedSetMixin, _SortedSetData, Set): +class SortedScalarSet(_ScalarOrderedSetMixin, SortedSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.SortedOrder) - _SortedSetData.__init__(self, component=self) + SortedSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index abd5a03c755..f62589a6873 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -84,7 +84,7 @@ SetData, FiniteSetData, InsertionOrderSetData, - _SortedSetData, + SortedSetData, _FiniteSetMixin, _OrderedSetMixin, SetInitializer, @@ -4173,9 +4173,9 @@ def test_indexed_set(self): self.assertTrue(m.I[1].isordered()) self.assertTrue(m.I[2].isordered()) self.assertTrue(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _SortedSetData) - self.assertIs(type(m.I[2]), _SortedSetData) - self.assertIs(type(m.I[3]), _SortedSetData) + self.assertIs(type(m.I[1]), SortedSetData) + self.assertIs(type(m.I[2]), SortedSetData) + self.assertIs(type(m.I[3]), SortedSetData) self.assertEqual(m.I.data(), {1: (2, 4, 5), 2: (2, 4, 5), 3: (2, 4, 5)}) # Explicit (procedural) construction From 63152a0be76006ff10e5f82f1b42de8af0ed6d56 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:57:39 -0600 Subject: [PATCH 0754/1178] Renamed _SOSConstraintData -> SOSConstraintData --- pyomo/contrib/appsi/base.py | 12 ++++++------ pyomo/contrib/appsi/fbbt.py | 6 +++--- pyomo/contrib/appsi/solvers/gurobi.py | 8 ++++---- pyomo/contrib/appsi/solvers/highs.py | 6 +++--- pyomo/contrib/appsi/writers/lp_writer.py | 6 +++--- pyomo/contrib/appsi/writers/nl_writer.py | 6 +++--- pyomo/contrib/solver/gurobi.py | 8 ++++---- pyomo/contrib/solver/persistent.py | 12 ++++++------ pyomo/core/base/sos.py | 15 ++++++++++----- pyomo/repn/plugins/cpxlp.py | 2 +- .../solvers/plugins/solvers/gurobi_persistent.py | 2 +- 11 files changed, 44 insertions(+), 39 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index e50d5201090..409c8e2596c 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -22,7 +22,7 @@ MutableMapping, ) from pyomo.core.base.constraint import GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData, Var from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block @@ -1034,10 +1034,10 @@ def add_constraints(self, cons: List[GeneralConstraintData]): v.fix() @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): pass - def add_sos_constraints(self, cons: List[_SOSConstraintData]): + def add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._vars_referenced_by_con: raise ValueError( @@ -1154,10 +1154,10 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): pass - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def remove_sos_constraints(self, cons: List[SOSConstraintData]): self._remove_sos_constraints(cons) for con in cons: if con not in self._vars_referenced_by_con: @@ -1339,7 +1339,7 @@ def update(self, timer: HierarchicalTimer = None): old_cons.append(c) else: assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) + c.ctype is None and isinstance(c, SOSConstraintData) ) old_sos.append(c) self.remove_constraints(old_cons) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 7735318f8ba..122ca5f7ffd 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -21,7 +21,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler @@ -169,7 +169,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): for c, cc in self._con_map.items(): cc.name = self._symbol_map.getSymbol(c, self._con_labeler) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError( 'IntervalTightener does not support SOS constraints' @@ -184,7 +184,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._cmodel.remove_constraint(cc) del self._rcon_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError( 'IntervalTightener does not support SOS constraints' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 4392cdf0839..8606d44cd46 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -25,7 +25,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import Var, GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn @@ -709,7 +709,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level @@ -749,7 +749,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._mutable_quadratic_helpers.pop(con, None) self._needs_updated = True - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1288,7 +1288,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a6b7c102c91..5af7b297684 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -22,7 +22,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn @@ -456,7 +456,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): np.array(coef_values, dtype=np.double), ) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if cons: raise NotImplementedError( 'Highs interface does not support SOS constraints' @@ -487,7 +487,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): {v: k for k, v in self._pyomo_con_to_solver_con_map.items()} ) - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if cons: raise NotImplementedError( 'Highs interface does not support SOS constraints' diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 3a168cdcd91..3a6682d5c00 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -14,7 +14,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value @@ -102,7 +102,7 @@ def _add_params(self, params: List[ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_lp_constraints(cons, self) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') @@ -113,7 +113,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._symbol_map.removeSymbol(c) del self._solver_con_to_pyomo_con_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index fced3c5ae10..c46cb1c0723 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -14,7 +14,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value @@ -126,7 +126,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): for c, cc in self._pyomo_con_to_solver_con_map.items(): cc.name = self._symbol_map.getSymbol(c, self._con_labeler) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') @@ -140,7 +140,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraint(cc) del self._solver_con_to_pyomo_con_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 107de15e625..c5a1c8c1cd8 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -24,7 +24,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn @@ -685,7 +685,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level @@ -725,7 +725,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._mutable_quadratic_helpers.pop(con, None) self._needs_updated = True - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1218,7 +1218,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 558b8cbf314..e98d76b4841 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -13,7 +13,7 @@ from typing import List from pyomo.core.base.constraint import GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData @@ -130,10 +130,10 @@ def add_constraints(self, cons: List[GeneralConstraintData]): v.fix() @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): pass - def add_sos_constraints(self, cons: List[_SOSConstraintData]): + def add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._vars_referenced_by_con: raise ValueError( @@ -230,10 +230,10 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): pass - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def remove_sos_constraints(self, cons: List[SOSConstraintData]): self._remove_sos_constraints(cons) for con in cons: if con not in self._vars_referenced_by_con: @@ -389,7 +389,7 @@ def update(self, timer: HierarchicalTimer = None): old_cons.append(c) else: assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) + c.ctype is None and isinstance(c, SOSConstraintData) ) old_sos.append(c) self.remove_constraints(old_cons) diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 6b8586c9b49..4a8afb05d71 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _SOSConstraintData(ActiveComponentData): +class SOSConstraintData(ActiveComponentData): """ This class defines the data for a single special ordered set. @@ -101,6 +101,11 @@ def set_items(self, variables, weights): self._weights.append(w) +class _SOSConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = SOSConstraintData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("SOS constraint expressions.") class SOSConstraint(ActiveIndexedComponent): """ @@ -512,10 +517,10 @@ def add(self, index, variables, weights=None): Add a component data for the specified index. """ if index is None: - # because ScalarSOSConstraint already makes an _SOSConstraintData instance + # because ScalarSOSConstraint already makes an SOSConstraintData instance soscondata = self else: - soscondata = _SOSConstraintData(self) + soscondata = SOSConstraintData(self) self._data[index] = soscondata soscondata._index = index @@ -549,9 +554,9 @@ def pprint(self, ostream=None, verbose=False, prefix=""): ostream.write("\t\t" + str(weight) + ' : ' + var.name + '\n') -class ScalarSOSConstraint(SOSConstraint, _SOSConstraintData): +class ScalarSOSConstraint(SOSConstraint, SOSConstraintData): def __init__(self, *args, **kwd): - _SOSConstraintData.__init__(self, self) + SOSConstraintData.__init__(self, self) SOSConstraint.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/repn/plugins/cpxlp.py b/pyomo/repn/plugins/cpxlp.py index 46e6b6d5265..6228e7c7286 100644 --- a/pyomo/repn/plugins/cpxlp.py +++ b/pyomo/repn/plugins/cpxlp.py @@ -374,7 +374,7 @@ def _print_expr_canonical( def printSOS(self, symbol_map, labeler, variable_symbol_map, soscondata, output): """ - Prints the SOS constraint associated with the _SOSConstraintData object + Prints the SOS constraint associated with the SOSConstraintData object """ sos_template_string = self.sos_template_string diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 8a81aad3d3e..585a78e3ef1 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -413,7 +413,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str From 430f98207353b1e1e7383504aa0336bc852aeb9c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:57:41 -0600 Subject: [PATCH 0755/1178] Renamed _SuffixData -> SuffixData --- pyomo/core/base/suffix.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index be2f732650d..c4b37789773 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -129,7 +129,7 @@ class SuffixDirection(enum.IntEnum): IMPORT_EXPORT = 3 -_SuffixDataTypeDomain = In(SuffixDataType) +SuffixDataTypeDomain = In(SuffixDataType) _SuffixDirectionDomain = In(SuffixDirection) @@ -253,7 +253,7 @@ def datatype(self): def datatype(self, datatype): """Set the suffix datatype.""" if datatype is not None: - datatype = _SuffixDataTypeDomain(datatype) + datatype = SuffixDataTypeDomain(datatype) self._datatype = datatype @property diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index c010cee5e54..1a49238f35b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -423,7 +423,7 @@ def _generate_symbol_map(self, info): return symbol_map -class _SuffixData(object): +class SuffixData(object): def __init__(self, name): self.name = name self.obj = {} @@ -505,6 +505,11 @@ def compile(self, column_order, row_order, obj_order, model_id): ) +class _SuffixData(metaclass=RenamedClass): + __renamed__new_class__ = SuffixData + __renamed__version__ = '6.7.2.dev0' + + class CachingNumericSuffixFinder(SuffixFinder): scale = True @@ -637,7 +642,7 @@ def write(self, model): continue name = suffix.local_name if name not in suffix_data: - suffix_data[name] = _SuffixData(name) + suffix_data[name] = SuffixData(name) suffix_data[name].update(suffix) # # Data structures to support variable/constraint scaling @@ -994,7 +999,7 @@ def write(self, model): "model. To avoid this error please use only one of " "these methods to define special ordered sets." ) - suffix_data[name] = _SuffixData(name) + suffix_data[name] = SuffixData(name) suffix_data[name].datatype.add(Suffix.INT) sos_id = 0 sosno = suffix_data['sosno'] @@ -1344,7 +1349,7 @@ def write(self, model): if not _vals: continue ostream.write(f"S{_field|_float} {len(_vals)} {name}\n") - # Note: _SuffixData.compile() guarantees the value is int/float + # Note: SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {_vals[_id]!r}\n" for _id in sorted(_vals)) ) @@ -1454,7 +1459,7 @@ def write(self, model): logger.warning("ignoring 'dual' suffix for Model") if data.con: ostream.write(f"d{len(data.con)}\n") - # Note: _SuffixData.compile() guarantees the value is int/float + # Note: SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {data.con[_id]!r}\n" for _id in sorted(data.con)) ) From f810600f0520097052fb4bf483920b86d03c80fb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 18:02:15 -0600 Subject: [PATCH 0756/1178] Renamed _VarData -> VarData --- pyomo/common/tests/test_timing.py | 4 +-- .../fme/fourier_motzkin_elimination.py | 4 +-- pyomo/contrib/gdp_bounds/info.py | 2 +- .../algorithms/solvers/pyomo_ext_cyipopt.py | 10 +++---- pyomo/contrib/pyros/config.py | 8 ++--- .../contrib/pyros/pyros_algorithm_methods.py | 2 +- pyomo/contrib/pyros/tests/test_config.py | 16 +++++----- pyomo/contrib/pyros/tests/test_grcs.py | 4 +-- pyomo/contrib/pyros/util.py | 6 ++-- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/boolean_var.py | 4 +-- pyomo/core/base/piecewise.py | 16 +++++----- pyomo/core/base/var.py | 29 +++++++++++-------- pyomo/core/beta/dict_objects.py | 4 +-- pyomo/core/beta/list_objects.py | 4 +-- pyomo/core/expr/numeric_expr.py | 2 +- .../plugins/transform/eliminate_fixed_vars.py | 4 +-- .../plugins/transform/radix_linearization.py | 6 ++-- pyomo/core/tests/unit/test_piecewise.py | 2 +- pyomo/core/tests/unit/test_var_set_bounds.py | 2 +- pyomo/dae/misc.py | 2 +- pyomo/repn/plugins/ampl/ampl_.py | 10 +++---- pyomo/repn/plugins/cpxlp.py | 2 +- pyomo/repn/plugins/mps.py | 2 +- pyomo/repn/plugins/nl_writer.py | 10 +++---- pyomo/repn/plugins/standard_form.py | 6 ++-- .../plugins/solvers/cplex_persistent.py | 4 +-- .../plugins/solvers/gurobi_persistent.py | 4 +-- .../plugins/solvers/mosek_persistent.py | 4 +-- .../plugins/solvers/persistent_solver.py | 4 +-- .../plugins/solvers/xpress_persistent.py | 4 +-- pyomo/util/calc_var_value.py | 2 +- 32 files changed, 95 insertions(+), 90 deletions(-) diff --git a/pyomo/common/tests/test_timing.py b/pyomo/common/tests/test_timing.py index 48288746882..90f4cdcd034 100644 --- a/pyomo/common/tests/test_timing.py +++ b/pyomo/common/tests/test_timing.py @@ -35,7 +35,7 @@ Any, TransformationFactory, ) -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData class _pseudo_component(Var): @@ -62,7 +62,7 @@ def test_raw_construction_timer(self): ) v = Var() v.construct() - a = ConstructionTimer(_VarData(v)) + a = ConstructionTimer(VarData(v)) self.assertRegex( str(a), r"ConstructionTimer object for Var ScalarVar\[NOTSET\]; " diff --git a/pyomo/contrib/fme/fourier_motzkin_elimination.py b/pyomo/contrib/fme/fourier_motzkin_elimination.py index a1b5d744cf4..4636450c58e 100644 --- a/pyomo/contrib/fme/fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/fourier_motzkin_elimination.py @@ -23,7 +23,7 @@ value, ConstraintList, ) -from pyomo.core.base import TransformationFactory, _VarData +from pyomo.core.base import TransformationFactory, VarData from pyomo.core.plugins.transform.hierarchy import Transformation from pyomo.common.config import ConfigBlock, ConfigValue, NonNegativeFloat from pyomo.common.modeling import unique_component_name @@ -58,7 +58,7 @@ def _check_var_bounds_filter(constraint): def vars_to_eliminate_list(x): - if isinstance(x, (Var, _VarData)): + if isinstance(x, (Var, VarData)): if not x.is_indexed(): return ComponentSet([x]) ans = ComponentSet() diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index db3f6d0846d..e65df2bfab0 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -35,7 +35,7 @@ def disjunctive_bound(var, scope): """Compute the disjunctive bounds for a variable in a given scope. Args: - var (_VarData): Variable for which to compute bound + var (VarData): Variable for which to compute bound scope (Component): The scope in which to compute the bound. If not a DisjunctData, it will walk up the tree and use the scope of the most immediate enclosing DisjunctData. diff --git a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py index 16c5a19a5c6..7f43f6ac7c0 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py @@ -16,7 +16,7 @@ from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP from pyomo.contrib.pynumero.sparse.block_vector import BlockVector from pyomo.environ import Var, Constraint, value -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData from pyomo.common.modeling import unique_component_name """ @@ -109,12 +109,12 @@ def __init__( An instance of a derived class (from ExternalInputOutputModel) that provides the methods to compute the outputs and the derivatives. - inputs : list of Pyomo variables (_VarData) + inputs : list of Pyomo variables (VarData) The Pyomo model needs to have variables to represent the inputs to the external model. This is the list of those input variables in the order that corresponds to the input_values vector provided in the set_inputs call. - outputs : list of Pyomo variables (_VarData) + outputs : list of Pyomo variables (VarData) The Pyomo model needs to have variables to represent the outputs from the external model. This is the list of those output variables in the order that corresponds to the numpy array returned from the evaluate_outputs call. @@ -130,7 +130,7 @@ def __init__( # verify that the inputs and outputs were passed correctly self._inputs = [v for v in inputs] for v in self._inputs: - if not isinstance(v, _VarData): + if not isinstance(v, VarData): raise RuntimeError( 'Argument inputs passed to PyomoExternalCyIpoptProblem must be' ' a list of VarData objects. Note: if you have an indexed variable, pass' @@ -139,7 +139,7 @@ def __init__( self._outputs = [v for v in outputs] for v in self._outputs: - if not isinstance(v, _VarData): + if not isinstance(v, VarData): raise RuntimeError( 'Argument outputs passed to PyomoExternalCyIpoptProblem must be' ' a list of VarData objects. Note: if you have an indexed variable, pass' diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index e60b474d037..59bf9a9ab37 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -16,7 +16,7 @@ Path, ) from pyomo.common.errors import ApplicationError, PyomoException -from pyomo.core.base import Var, _VarData +from pyomo.core.base import Var, VarData from pyomo.core.base.param import Param, ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger @@ -98,7 +98,7 @@ class InputDataStandardizer(object): Pyomo component type, such as Component, Var or Param. cdatatype : type Corresponding Pyomo component data type, such as - _ComponentData, _VarData, or ParamData. + _ComponentData, VarData, or ParamData. ctype_validator : callable, optional Validator function for objects of type `ctype`. cdatatype_validator : callable, optional @@ -511,7 +511,7 @@ def pyros_config(): "first_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), + domain=InputDataStandardizer(Var, VarData, allow_repeats=False), description="First-stage (or design) variables.", visibility=1, ), @@ -520,7 +520,7 @@ def pyros_config(): "second_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), + domain=InputDataStandardizer(Var, VarData, allow_repeats=False), description="Second-stage (or control) variables.", visibility=1, ), diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 5987db074e6..cfb57b08c7f 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -28,7 +28,7 @@ from pyomo.core.base import value from pyomo.core.expr import MonomialTermExpression from pyomo.common.collections import ComponentSet, ComponentMap -from pyomo.core.base.var import _VarData as VarData +from pyomo.core.base.var import VarData as VarData from itertools import chain from pyomo.common.dependencies import numpy as np diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index cd635e795fc..166fbada4ff 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -5,7 +5,7 @@ import logging import unittest -from pyomo.core.base import ConcreteModel, Var, _VarData +from pyomo.core.base import ConcreteModel, Var, VarData from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, ParamData @@ -38,7 +38,7 @@ def test_single_component_data(self): mdl = ConcreteModel() mdl.v = Var([0, 1]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) standardizer_input = mdl.v[0] standardizer_output = standardizer_func(standardizer_input) @@ -74,7 +74,7 @@ def test_standardizer_indexed_component(self): mdl = ConcreteModel() mdl.v = Var([0, 1]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) standardizer_input = mdl.v standardizer_output = standardizer_func(standardizer_input) @@ -113,7 +113,7 @@ def test_standardizer_multiple_components(self): mdl.v = Var([0, 1]) mdl.x = Var(["a", "b"]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) standardizer_input = [mdl.v[0], mdl.x] standardizer_output = standardizer_func(standardizer_input) @@ -154,7 +154,7 @@ def test_standardizer_invalid_duplicates(self): mdl.v = Var([0, 1]) mdl.x = Var(["a", "b"]) - standardizer_func = InputDataStandardizer(Var, _VarData, allow_repeats=False) + standardizer_func = InputDataStandardizer(Var, VarData, allow_repeats=False) exc_str = r"Standardized.*list.*contains duplicate entries\." with self.assertRaisesRegex(ValueError, exc_str): @@ -165,7 +165,7 @@ def test_standardizer_invalid_type(self): Test standardizer raises exception as expected when input is of invalid type. """ - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) exc_str = r"Input object .*is not of valid component type.*" with self.assertRaisesRegex(TypeError, exc_str): @@ -178,7 +178,7 @@ def test_standardizer_iterable_with_invalid_type(self): """ mdl = ConcreteModel() mdl.v = Var([0, 1]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) exc_str = r"Input object .*entry of iterable.*is not of valid component type.*" with self.assertRaisesRegex(TypeError, exc_str): @@ -189,7 +189,7 @@ def test_standardizer_invalid_str_passed(self): Test standardizer raises exception as expected when input is of invalid type str. """ - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) exc_str = r"Input object .*is not of valid component type.*" with self.assertRaisesRegex(TypeError, exc_str): diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c308f0d6990..8093e93c8ef 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -19,7 +19,7 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData from pyomo.core.expr import ( identify_variables, identify_mutable_parameters, @@ -592,7 +592,7 @@ def test_dr_eqns_form_correct(self): param_product_multiplicand = term.args[0] dr_var_multiplicand = term.args[1] else: - self.assertIsInstance(term, _VarData) + self.assertIsInstance(term, VarData) param_product_multiplicand = 1 dr_var_multiplicand = term diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a3ab3464aa8..65fb0a2c6aa 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -832,7 +832,7 @@ def get_state_vars(blk, first_stage_variables, second_stage_variables): Get state variables of a modeling block. The state variables with respect to `blk` are the unfixed - `_VarData` objects participating in the active objective + `VarData` objects participating in the active objective or constraints descended from `blk` which are not first-stage variables or second-stage variables. @@ -847,7 +847,7 @@ def get_state_vars(blk, first_stage_variables, second_stage_variables): Yields ------ - _VarData + VarData State variable. """ dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( @@ -954,7 +954,7 @@ def validate_variable_partitioning(model, config): Returns ------- - list of _VarData + list of VarData State variables of the model. Raises diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 408cf16c00e..0363380af1f 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -57,7 +57,7 @@ from pyomo.core.base.check import BuildCheck from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, _VarData, GeneralVarData, ScalarVar, VarList +from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 287851a7f7e..925dca530a7 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -270,14 +270,14 @@ def stale(self, val): self._stale = StaleFlagManager.get_flag(0) def get_associated_binary(self): - """Get the binary _VarData associated with this + """Get the binary VarData associated with this GeneralBooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary _VarData to this GeneralBooleanVarData""" + """Associate a binary VarData to this GeneralBooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index 43f8ddbfef5..f061ebfbdc8 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -47,7 +47,7 @@ from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.constraint import Constraint, ConstraintList from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.var import Var, _VarData, IndexedVar +from pyomo.core.base.var import Var, VarData, IndexedVar from pyomo.core.base.set_types import PositiveReals, NonNegativeReals, Binary from pyomo.core.base.util import flatten_tuple @@ -1240,7 +1240,7 @@ def __init__(self, *args, **kwds): # Check that the variables args are actually Pyomo Vars if not ( - isinstance(self._domain_var, _VarData) + isinstance(self._domain_var, VarData) or isinstance(self._domain_var, IndexedVar) ): msg = ( @@ -1249,7 +1249,7 @@ def __init__(self, *args, **kwds): ) raise TypeError(msg % (repr(self._domain_var),)) if not ( - isinstance(self._range_var, _VarData) + isinstance(self._range_var, VarData) or isinstance(self._range_var, IndexedVar) ): msg = ( @@ -1359,22 +1359,22 @@ def add(self, index, _is_indexed=None): _self_yvar = None _self_domain_pts_index = None if not _is_indexed: - # allows one to mix Var and _VarData as input to + # allows one to mix Var and VarData as input to # non-indexed Piecewise, index would be None in this case - # so for Var elements Var[None] is Var, but _VarData[None] would fail + # so for Var elements Var[None] is Var, but VarData[None] would fail _self_xvar = self._domain_var _self_yvar = self._range_var _self_domain_pts_index = self._domain_points[index] else: - # The following allows one to specify a Var or _VarData + # The following allows one to specify a Var or VarData # object even with an indexed Piecewise component. # The most common situation will most likely be a VarArray, # so we try this first. - if not isinstance(self._domain_var, _VarData): + if not isinstance(self._domain_var, VarData): _self_xvar = self._domain_var[index] else: _self_xvar = self._domain_var - if not isinstance(self._range_var, _VarData): + if not isinstance(self._range_var, VarData): _self_yvar = self._range_var[index] else: _self_yvar = self._range_var diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 0e45ad44225..509238e4e6b 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -88,7 +88,7 @@ ) -class _VarData(ComponentData, NumericValue): +class VarData(ComponentData, NumericValue): """This class defines the abstract interface for a single variable. Note that this "abstract" class is not intended to be directly @@ -319,7 +319,12 @@ def free(self): return self.unfix() -class GeneralVarData(_VarData): +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData + __renamed__version__ = '6.7.2.dev0' + + +class GeneralVarData(VarData): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -329,7 +334,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _VarData + # - VarData # - ComponentData # - NumericValue self._component = weakref_ref(component) if (component is not None) else None @@ -448,9 +453,9 @@ def domain(self, domain): ) raise - @_VarData.bounds.getter + @VarData.bounds.getter def bounds(self): - # Custom implementation of _VarData.bounds to avoid unnecessary + # Custom implementation of VarData.bounds to avoid unnecessary # expression generation and duplicate calls to domain.bounds() domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -491,9 +496,9 @@ def bounds(self): ub = min(ub, domain_ub) return lb, ub - @_VarData.lb.getter + @VarData.lb.getter def lb(self): - # Custom implementation of _VarData.lb to avoid unnecessary + # Custom implementation of VarData.lb to avoid unnecessary # expression generation domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -516,9 +521,9 @@ def lb(self): lb = max(lb, domain_lb) return lb - @_VarData.ub.getter + @VarData.ub.getter def ub(self): - # Custom implementation of _VarData.ub to avoid unnecessary + # Custom implementation of VarData.ub to avoid unnecessary # expression generation domain_lb, domain_ub = self.domain.bounds() # ub is the tighter of the domain and bounds @@ -780,7 +785,7 @@ def add(self, index): def construct(self, data=None): """ - Construct the _VarData objects for this variable + Construct the VarData objects for this variable """ if self._constructed: return @@ -839,7 +844,7 @@ def construct(self, data=None): # initializers that are constant, we can avoid # re-calling (and re-validating) the inputs in certain # cases. To support this, we will create the first - # _VarData and then use it as a template to initialize + # VarData and then use it as a template to initialize # (constant portions of) every VarData so as to not # repeat all the domain/bounds validation. try: @@ -1008,7 +1013,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Unfix all variables in this :class:`IndexedVar` (treat as variable) - This sets the :attr:`_VarData.fixed` indicator to False for + This sets the :attr:`VarData.fixed` indicator to False for every variable in this :class:`IndexedVar`. """ diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index 7c44166f189..eedb3c45bf3 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -14,7 +14,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any -from pyomo.core.base.var import IndexedVar, _VarData +from pyomo.core.base.var import IndexedVar, VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData @@ -184,7 +184,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _VarData, *args, **kwds) + ComponentDict.__init__(self, VarData, *args, **kwds) class ConstraintDict(ComponentDict, IndexedConstraint): diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index d10a30e18e2..005bfc38a1f 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -14,7 +14,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any -from pyomo.core.base.var import IndexedVar, _VarData +from pyomo.core.base.var import IndexedVar, VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData @@ -232,7 +232,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _VarData, *args, **kwds) + ComponentList.__init__(self, VarData, *args, **kwds) class XConstraintList(ComponentList, IndexedConstraint): diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 25d83ca20f4..50abaeedbba 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1238,7 +1238,7 @@ class LinearExpression(SumExpression): - not potentially variable (e.g., native types, Params, or NPV expressions) - :py:class:`MonomialTermExpression` - - :py:class:`_VarData` + - :py:class:`VarData` Args: args (tuple): Children nodes diff --git a/pyomo/core/plugins/transform/eliminate_fixed_vars.py b/pyomo/core/plugins/transform/eliminate_fixed_vars.py index 9312035b8c8..934228afd7c 100644 --- a/pyomo/core/plugins/transform/eliminate_fixed_vars.py +++ b/pyomo/core/plugins/transform/eliminate_fixed_vars.py @@ -11,7 +11,7 @@ from pyomo.core.expr import ExpressionBase, as_numeric from pyomo.core import Constraint, Objective, TransformationFactory -from pyomo.core.base.var import Var, _VarData +from pyomo.core.base.var import Var, VarData from pyomo.core.util import sequence from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation @@ -77,7 +77,7 @@ def _fix_vars(self, expr, model): if isinstance(expr._args[i], ExpressionBase): _args.append(self._fix_vars(expr._args[i], model)) elif ( - isinstance(expr._args[i], Var) or isinstance(expr._args[i], _VarData) + isinstance(expr._args[i], Var) or isinstance(expr._args[i], VarData) ) and expr._args[i].fixed: if expr._args[i].value != 0.0: _args.append(as_numeric(expr._args[i].value)) diff --git a/pyomo/core/plugins/transform/radix_linearization.py b/pyomo/core/plugins/transform/radix_linearization.py index c67e556d60c..92270655f31 100644 --- a/pyomo/core/plugins/transform/radix_linearization.py +++ b/pyomo/core/plugins/transform/radix_linearization.py @@ -21,7 +21,7 @@ Block, RangeSet, ) -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData import logging @@ -268,8 +268,8 @@ def _collect_bilinear(self, expr, bilin, quad): self._collect_bilinear(e, bilin, quad) # No need to check denominator, as this is poly_degree==2 return - if not isinstance(expr._numerator[0], _VarData) or not isinstance( - expr._numerator[1], _VarData + if not isinstance(expr._numerator[0], VarData) or not isinstance( + expr._numerator[1], VarData ): raise RuntimeError("Cannot yet handle complex subexpressions") if expr._numerator[0] is expr._numerator[1]: diff --git a/pyomo/core/tests/unit/test_piecewise.py b/pyomo/core/tests/unit/test_piecewise.py index af82ef7c06d..7b8e01e6a45 100644 --- a/pyomo/core/tests/unit/test_piecewise.py +++ b/pyomo/core/tests/unit/test_piecewise.py @@ -104,7 +104,7 @@ def test_indexed_with_nonindexed_vars(self): model.con3 = Piecewise(*args, **keywords) # test that nonindexed Piecewise can handle - # _VarData (e.g model.x[1] + # VarData (e.g model.x[1] def test_nonindexed_with_indexed_vars(self): model = ConcreteModel() model.range = Var([1]) diff --git a/pyomo/core/tests/unit/test_var_set_bounds.py b/pyomo/core/tests/unit/test_var_set_bounds.py index bae89556ce3..1686ba4f1c6 100644 --- a/pyomo/core/tests/unit/test_var_set_bounds.py +++ b/pyomo/core/tests/unit/test_var_set_bounds.py @@ -36,7 +36,7 @@ # GAH: These tests been temporarily disabled. It is no longer the job of Var # to validate its domain at the time of construction. It only needs to # ensure that whatever object is passed as its domain is suitable for -# interacting with the _VarData interface (e.g., has a bounds method) +# interacting with the VarData interface (e.g., has a bounds method) # The plan is to start adding functionality to the solver interfaces # that will support custom domains. diff --git a/pyomo/dae/misc.py b/pyomo/dae/misc.py index 3e09a055577..dcb73f60c9e 100644 --- a/pyomo/dae/misc.py +++ b/pyomo/dae/misc.py @@ -263,7 +263,7 @@ def _update_var(v): # Note: This is not required it is handled by the _default method on # Var (which is now a IndexedComponent). However, it # would be much slower to rely on that method to generate new - # _VarData for a large number of new indices. + # VarData for a large number of new indices. new_indices = set(v.index_set()) - set(v._data.keys()) for index in new_indices: v.add(index) diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index 840bee2166c..1cff45b30c1 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -168,8 +168,8 @@ def _build_op_template(): _op_template[EXPR.EqualityExpression] = "o24{C}\n" _op_comment[EXPR.EqualityExpression] = "\t#eq" - _op_template[var._VarData] = "v%d{C}\n" - _op_comment[var._VarData] = "\t#%s" + _op_template[var.VarData] = "v%d{C}\n" + _op_comment[var.VarData] = "\t#%s" _op_template[param.ParamData] = "n%r{C}\n" _op_comment[param.ParamData] = "" @@ -733,16 +733,16 @@ def _print_nonlinear_terms_NL(self, exp): % (exp_type) ) - elif isinstance(exp, (var._VarData, IVariable)) and (not exp.is_fixed()): + elif isinstance(exp, (var.VarData, IVariable)) and (not exp.is_fixed()): # (self._output_fixed_variable_bounds or if not self._symbolic_solver_labels: OUTPUT.write( - self._op_string[var._VarData] + self._op_string[var.VarData] % (self.ampl_var_id[self._varID_map[id(exp)]]) ) else: OUTPUT.write( - self._op_string[var._VarData] + self._op_string[var.VarData] % ( self.ampl_var_id[self._varID_map[id(exp)]], self._name_labeler(exp), diff --git a/pyomo/repn/plugins/cpxlp.py b/pyomo/repn/plugins/cpxlp.py index 6228e7c7286..45f4279f8fe 100644 --- a/pyomo/repn/plugins/cpxlp.py +++ b/pyomo/repn/plugins/cpxlp.py @@ -60,7 +60,7 @@ def __init__(self): # The LP writer tracks which variables are # referenced in constraints, so that a user does not end up with a # zillion "unreferenced variables" warning messages. - # This dictionary maps id(_VarData) -> _VarData. + # This dictionary maps id(VarData) -> VarData. self._referenced_variable_ids = {} # Per ticket #4319, we are using %.17g, which mocks the diff --git a/pyomo/repn/plugins/mps.py b/pyomo/repn/plugins/mps.py index ba26783eea1..e1a0d2187fc 100644 --- a/pyomo/repn/plugins/mps.py +++ b/pyomo/repn/plugins/mps.py @@ -62,7 +62,7 @@ def __init__(self, int_marker=False): # referenced in constraints, so that one doesn't end up with a # zillion "unreferenced variables" warning messages. stored at # the object level to avoid additional method arguments. - # dictionary of id(_VarData)->_VarData. + # dictionary of id(VarData)->VarData. self._referenced_variable_ids = {} # Keven Hunter made a nice point about using %.16g in his attachment diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 1a49238f35b..e1691e75f2f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -77,7 +77,7 @@ ObjectiveData, ) from pyomo.core.base.suffix import SuffixFinder -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData import pyomo.core.kernel as kernel from pyomo.core.pyomoobject import PyomoObject from pyomo.opt import WriterFactory @@ -129,7 +129,7 @@ class NLWriterInfo(object): Attributes ---------- - variables: List[_VarData] + variables: List[VarData] The list of (unfixed) Pyomo model variables in the order written to the NL file @@ -162,10 +162,10 @@ class NLWriterInfo(object): file in the same order as the :py:attr:`variables` and generated .col file. - eliminated_vars: List[Tuple[_VarData, NumericExpression]] + eliminated_vars: List[Tuple[VarData, NumericExpression]] The list of variables in the model that were eliminated by the - presolve. Each entry is a 2-tuple of (:py:class:`_VarData`, + presolve. Each entry is a 2-tuple of (:py:class:`VarData`, :py:class`NumericExpression`|`float`). The list is in the necessary order for correct evaluation (i.e., all variables appearing in the expression must either have been sent to the @@ -466,7 +466,7 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (_VarData, ConstraintData, ObjectiveData)): + elif isinstance(obj, (VarData, ConstraintData, ObjectiveData)): missing_component_data.add(obj) elif isinstance(obj, (Var, Constraint, Objective)): # Expand this indexed component to store the diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index e6dc217acc9..434a9b8f35a 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -84,17 +84,17 @@ class LinearStandardFormInfo(object): +/- 1 indicating if the row was multiplied by -1 (corresponding to a constraint lower bound) or +1 (upper bound). - columns : List[_VarData] + columns : List[VarData] The list of Pyomo variable objects corresponding to columns in the `A` and `c` matrices. - eliminated_vars: List[Tuple[_VarData, NumericExpression]] + eliminated_vars: List[Tuple[VarData, NumericExpression]] The list of variables from the original model that do not appear in the standard form (usually because they were replaced by nonnegative variables). Each entry is a 2-tuple of - (:py:class:`_VarData`, :py:class`NumericExpression`|`float`). + (:py:class:`VarData`, :py:class`NumericExpression`|`float`). The list is in the necessary order for correct evaluation (i.e., all variables appearing in the expression must either have appeared in the standard form, or appear *earlier* in this list. diff --git a/pyomo/solvers/plugins/solvers/cplex_persistent.py b/pyomo/solvers/plugins/solvers/cplex_persistent.py index fd396a8c87f..754dadc09e2 100644 --- a/pyomo/solvers/plugins/solvers/cplex_persistent.py +++ b/pyomo/solvers/plugins/solvers/cplex_persistent.py @@ -82,7 +82,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -130,7 +130,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 585a78e3ef1..97a3533c3f9 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -111,7 +111,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -710,7 +710,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/plugins/solvers/mosek_persistent.py b/pyomo/solvers/plugins/solvers/mosek_persistent.py index 9e7f8de1b41..efcbb7dd9dd 100644 --- a/pyomo/solvers/plugins/solvers/mosek_persistent.py +++ b/pyomo/solvers/plugins/solvers/mosek_persistent.py @@ -95,7 +95,7 @@ def remove_var(self, solver_var): This will keep any other model components intact. Parameters ---------- - solver_var: Var (scalar Var or single _VarData) + solver_var: Var (scalar Var or single VarData) """ self.remove_vars(solver_var) @@ -106,7 +106,7 @@ def remove_vars(self, *solver_vars): This will keep any other model components intact. Parameters ---------- - *solver_var: Var (scalar Var or single _VarData) + *solver_var: Var (scalar Var or single VarData) """ try: var_ids = [] diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index 79cd669dd71..3c2a9e52eab 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -206,7 +206,7 @@ def add_column(self, model, var, obj_coef, constraints, coefficients): Parameters ---------- model: pyomo ConcreteModel to which the column will be added - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float, pyo.Param constraints: list of scalar Constraints of single ConstraintDatas coefficients: list of the coefficient to put on var in the associated constraint @@ -380,7 +380,7 @@ def remove_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed diff --git a/pyomo/solvers/plugins/solvers/xpress_persistent.py b/pyomo/solvers/plugins/solvers/xpress_persistent.py index 513a7fbc257..fbdc2866dcf 100644 --- a/pyomo/solvers/plugins/solvers/xpress_persistent.py +++ b/pyomo/solvers/plugins/solvers/xpress_persistent.py @@ -90,7 +90,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -124,7 +124,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index d5bceb5c67b..254b82c59cd 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -53,7 +53,7 @@ def calculate_variable_from_constraint( Parameters: ----------- - variable: :py:class:`_VarData` + variable: :py:class:`VarData` The variable to solve for constraint: :py:class:`ConstraintData` or relational expression or `tuple` The equality constraint to use to solve for the variable value. From 0e994faacb398642756fdd12cf0802a49cf92dc4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 18:06:48 -0600 Subject: [PATCH 0757/1178] Revert "Renamed _SuffixData -> SuffixData" This reverts commit 430f98207353b1e1e7383504aa0336bc852aeb9c. --- pyomo/core/base/suffix.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index c4b37789773..be2f732650d 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -129,7 +129,7 @@ class SuffixDirection(enum.IntEnum): IMPORT_EXPORT = 3 -SuffixDataTypeDomain = In(SuffixDataType) +_SuffixDataTypeDomain = In(SuffixDataType) _SuffixDirectionDomain = In(SuffixDirection) @@ -253,7 +253,7 @@ def datatype(self): def datatype(self, datatype): """Set the suffix datatype.""" if datatype is not None: - datatype = SuffixDataTypeDomain(datatype) + datatype = _SuffixDataTypeDomain(datatype) self._datatype = datatype @property diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e1691e75f2f..23e14104b89 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -423,7 +423,7 @@ def _generate_symbol_map(self, info): return symbol_map -class SuffixData(object): +class _SuffixData(object): def __init__(self, name): self.name = name self.obj = {} @@ -505,11 +505,6 @@ def compile(self, column_order, row_order, obj_order, model_id): ) -class _SuffixData(metaclass=RenamedClass): - __renamed__new_class__ = SuffixData - __renamed__version__ = '6.7.2.dev0' - - class CachingNumericSuffixFinder(SuffixFinder): scale = True @@ -642,7 +637,7 @@ def write(self, model): continue name = suffix.local_name if name not in suffix_data: - suffix_data[name] = SuffixData(name) + suffix_data[name] = _SuffixData(name) suffix_data[name].update(suffix) # # Data structures to support variable/constraint scaling @@ -999,7 +994,7 @@ def write(self, model): "model. To avoid this error please use only one of " "these methods to define special ordered sets." ) - suffix_data[name] = SuffixData(name) + suffix_data[name] = _SuffixData(name) suffix_data[name].datatype.add(Suffix.INT) sos_id = 0 sosno = suffix_data['sosno'] @@ -1349,7 +1344,7 @@ def write(self, model): if not _vals: continue ostream.write(f"S{_field|_float} {len(_vals)} {name}\n") - # Note: SuffixData.compile() guarantees the value is int/float + # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {_vals[_id]!r}\n" for _id in sorted(_vals)) ) @@ -1459,7 +1454,7 @@ def write(self, model): logger.warning("ignoring 'dual' suffix for Model") if data.con: ostream.write(f"d{len(data.con)}\n") - # Note: SuffixData.compile() guarantees the value is int/float + # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {data.con[_id]!r}\n" for _id in sorted(data.con)) ) From b1a7b30cecf901ce0e1c4a9c146814b250e4cc75 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 19:44:54 -0600 Subject: [PATCH 0758/1178] Update ComponentData imports, provide deprecation paths --- pyomo/core/base/__init__.py | 110 +++++++++++++++++++++-------------- pyomo/core/base/piecewise.py | 2 +- pyomo/core/base/sets.py | 2 +- pyomo/gdp/__init__.py | 8 ++- 4 files changed, 75 insertions(+), 47 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 0363380af1f..9a06a3e02bb 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -33,10 +33,14 @@ BooleanValue, native_logical_values, ) + from pyomo.core.kernel.objective import minimize, maximize -from pyomo.core.base.config import PyomoOptions -from pyomo.core.base.expression import Expression, ExpressionData +from pyomo.core.base.component import name, Component, ModelComponentFactory +from pyomo.core.base.componentuid import ComponentUID +from pyomo.core.base.config import PyomoOptions +from pyomo.core.base.enums import SortComponents, TraversalStrategy +from pyomo.core.base.instance2dat import instance2dat from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, @@ -47,17 +51,37 @@ NameLabeler, ShortNameLabeler, ) +from pyomo.core.base.misc import display +from pyomo.core.base.reference import Reference +from pyomo.core.base.symbol_map import symbol_map_from_instance +from pyomo.core.base.transformation import ( + Transformation, + TransformationFactory, + ReverseTransformationToken, +) + +from pyomo.core.base.PyomoModel import ( + global_option, + ModelSolution, + ModelSolutions, + Model, + ConcreteModel, + AbstractModel, +) # # Components # -from pyomo.core.base.component import name, Component, ModelComponentFactory -from pyomo.core.base.componentuid import ComponentUID from pyomo.core.base.action import BuildAction -from pyomo.core.base.check import BuildCheck -from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet -from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList +from pyomo.core.base.block import ( + Block, + BlockData, + ScalarBlock, + active_components, + components, + active_components_data, + components_data, +) from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, @@ -65,6 +89,8 @@ BooleanVarList, ScalarBooleanVar, ) +from pyomo.core.base.check import BuildCheck +from pyomo.core.base.connector import Connector, ConnectorData from pyomo.core.base.constraint import ( simple_constraint_rule, simple_constraintlist_rule, @@ -72,6 +98,8 @@ Constraint, ConstraintData, ) +from pyomo.core.base.expression import Expression, ExpressionData +from pyomo.core.base.external import ExternalFunction from pyomo.core.base.logical_constraint import ( LogicalConstraint, LogicalConstraintList, @@ -84,19 +112,13 @@ ObjectiveList, ObjectiveData, ) -from pyomo.core.base.connector import Connector -from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.piecewise import Piecewise -from pyomo.core.base.suffix import ( - active_export_suffix_generator, - active_import_suffix_generator, - Suffix, -) -from pyomo.core.base.external import ExternalFunction -from pyomo.core.base.symbol_map import symbol_map_from_instance -from pyomo.core.base.reference import Reference - +from pyomo.core.base.param import Param, ParamData +from pyomo.core.base.piecewise import Piecewise, PiecewiseData from pyomo.core.base.set import ( + Set, + SetData, + SetOf, + RangeSet, Reals, PositiveReals, NonPositiveReals, @@ -116,34 +138,19 @@ PercentFraction, RealInterval, IntegerInterval, + simple_set_rule, ) -from pyomo.core.base.misc import display -from pyomo.core.base.block import ( - Block, - ScalarBlock, - active_components, - components, - active_components_data, - components_data, -) -from pyomo.core.base.enums import SortComponents, TraversalStrategy -from pyomo.core.base.PyomoModel import ( - global_option, - ModelSolution, - ModelSolutions, - Model, - ConcreteModel, - AbstractModel, -) -from pyomo.core.base.transformation import ( - Transformation, - TransformationFactory, - ReverseTransformationToken, +from pyomo.core.base.sos import SOSConstraint, SOSConstraintData +from pyomo.core.base.suffix import ( + active_export_suffix_generator, + active_import_suffix_generator, + Suffix, ) +from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList -from pyomo.core.base.instance2dat import instance2dat - +# # These APIs are deprecated and should be removed in the near future +# from pyomo.core.base.set import set_options, RealSet, IntegerSet, BooleanSet from pyomo.common.deprecation import relocated_module_attribute @@ -155,4 +162,19 @@ relocated_module_attribute( 'SimpleBooleanVar', 'pyomo.core.base.boolean_var.SimpleBooleanVar', version='6.0' ) +# Historically, only a subset of "private" component data classes were imported here +for _cdata in ( + 'ConstraintData', + 'LogicalConstraintData', + 'ExpressionData', + 'VarData', + 'GeneralVarData', + 'GeneralBooleanVarData', + 'BooleanVarData', + 'ObjectiveData', +): + relocated_module_attribute( + f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2.dev0' + ) +del _cdata del relocated_module_attribute diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index f061ebfbdc8..efe500dbfb1 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -40,7 +40,7 @@ import enum from pyomo.common.log import is_debug_set -from pyomo.common.deprecation import deprecation_warning +from pyomo.common.deprecation import RenamedClass, deprecation_warning from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer from pyomo.core.base.block import Block, BlockData diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index 3ebdc6875d1..72d49479dd3 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -17,7 +17,7 @@ process_setarg, set_options, simple_set_rule, - SetDataBase, + _SetDataBase, SetData, Set, SetOf, diff --git a/pyomo/gdp/__init__.py b/pyomo/gdp/__init__.py index a18bc03084a..d204369cdba 100644 --- a/pyomo/gdp/__init__.py +++ b/pyomo/gdp/__init__.py @@ -9,7 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.gdp.disjunct import GDP_Error, Disjunct, Disjunction +from pyomo.gdp.disjunct import ( + GDP_Error, + Disjunct, + DisjunctData, + Disjunction, + DisjunctionData, +) # Do not import these files: importing them registers the transformation # plugins with the pyomo script so that they get automatically invoked. From 1e5b54e7a30ee6ad9de1a956bd1f975148c7efd9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:17:28 -0600 Subject: [PATCH 0759/1178] Mrege GeneralVarData into VarData class --- pyomo/core/base/var.py | 439 +++++++++++++++++------------------------ 1 file changed, 183 insertions(+), 256 deletions(-) diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 509238e4e6b..b1634b61c44 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -85,246 +85,11 @@ 'value', 'stale', 'fixed', + ('__call__', "access property 'value' on"), ) class VarData(ComponentData, NumericValue): - """This class defines the abstract interface for a single variable. - - Note that this "abstract" class is not intended to be directly - instantiated. - - """ - - __slots__ = () - - # - # Interface - # - - def has_lb(self): - """Returns :const:`False` when the lower bound is - :const:`None` or negative infinity""" - return self.lb is not None - - def has_ub(self): - """Returns :const:`False` when the upper bound is - :const:`None` or positive infinity""" - return self.ub is not None - - # TODO: deprecate this? Properties are generally preferred over "set*()" - def setlb(self, val): - """ - Set the lower bound for this variable after validating that - the value is fixed (or None). - """ - self.lower = val - - # TODO: deprecate this? Properties are generally preferred over "set*()" - def setub(self, val): - """ - Set the upper bound for this variable after validating that - the value is fixed (or None). - """ - self.upper = val - - @property - def bounds(self): - """Returns (or set) the tuple (lower bound, upper bound). - - This returns the current (numeric) values of the lower and upper - bounds as a tuple. If there is no bound, returns None (and not - +/-inf) - - """ - return self.lb, self.ub - - @bounds.setter - def bounds(self, val): - self.lower, self.upper = val - - @property - def lb(self): - """Return (or set) the numeric value of the variable lower bound.""" - lb = value(self.lower) - return None if lb == _ninf else lb - - @lb.setter - def lb(self, val): - self.lower = val - - @property - def ub(self): - """Return (or set) the numeric value of the variable upper bound.""" - ub = value(self.upper) - return None if ub == _inf else ub - - @ub.setter - def ub(self, val): - self.upper = val - - def is_integer(self): - """Returns True when the domain is a contiguous integer range.""" - _id = id(self.domain) - if _id in _known_global_real_domains: - return not _known_global_real_domains[_id] - _interval = self.domain.get_interval() - if _interval is None: - return False - # Note: it is not sufficient to just check the step: the - # starting / ending points must be integers (or not specified) - start, stop, step = _interval - return ( - step == 1 - and (start is None or int(start) == start) - and (stop is None or int(stop) == stop) - ) - - def is_binary(self): - """Returns True when the domain is restricted to Binary values.""" - domain = self.domain - if domain is Binary: - return True - if id(domain) in _known_global_real_domains: - return False - return domain.get_interval() == (0, 1, 1) - - def is_continuous(self): - """Returns True when the domain is a continuous real range""" - _id = id(self.domain) - if _id in _known_global_real_domains: - return _known_global_real_domains[_id] - _interval = self.domain.get_interval() - return _interval is not None and _interval[2] == 0 - - def is_fixed(self): - """Returns True if this variable is fixed, otherwise returns False.""" - return self.fixed - - def is_constant(self): - """Returns False because this is not a constant in an expression.""" - return False - - def is_variable_type(self): - """Returns True because this is a variable.""" - return True - - def is_potentially_variable(self): - """Returns True because this is a variable.""" - return True - - def _compute_polynomial_degree(self, result): - """ - If the variable is fixed, it represents a constant - is a polynomial with degree 0. Otherwise, it has - degree 1. This method is used in expressions to - compute polynomial degree. - """ - if self.fixed: - return 0 - return 1 - - def clear(self): - self.value = None - - def __call__(self, exception=True): - """Compute the value of this variable.""" - return self.value - - # - # Abstract Interface - # - - def set_value(self, val, skip_validation=False): - """Set the current variable value.""" - raise NotImplementedError - - @property - def value(self): - """Return (or set) the value for this variable.""" - raise NotImplementedError - - @property - def domain(self): - """Return (or set) the domain for this variable.""" - raise NotImplementedError - - @property - def lower(self): - """Return (or set) an expression for the variable lower bound.""" - raise NotImplementedError - - @property - def upper(self): - """Return (or set) an expression for the variable upper bound.""" - raise NotImplementedError - - @property - def fixed(self): - """Return (or set) the fixed indicator for this variable. - - Alias for :meth:`is_fixed` / :meth:`fix` / :meth:`unfix`. - - """ - raise NotImplementedError - - @property - def stale(self): - """The stale status for this variable. - - Variables are "stale" if their current value was not updated as - part of the most recent model update. A "model update" can be - one of several things: a solver invocation, loading a previous - solution, or manually updating a non-stale :class:`Var` value. - - Returns - ------- - bool - - Notes - ----- - Fixed :class:`Var` objects will be stale after invoking a solver - (as their value was not updated by the solver). - - Updating a stale :class:`Var` value will not cause other - variable values to be come stale. However, updating the first - non-stale :class:`Var` value after a solve or solution load - *will* cause all other variables to be marked as stale - - """ - raise NotImplementedError - - def fix(self, value=NOTSET, skip_validation=False): - """Fix the value of this variable (treat as nonvariable) - - This sets the :attr:`fixed` indicator to True. If ``value`` is - provided, the value (and the ``skip_validation`` flag) are first - passed to :meth:`set_value()`. - - """ - self.fixed = True - if value is not NOTSET: - self.set_value(value, skip_validation) - - def unfix(self): - """Unfix this variable (treat as variable in solver interfaces) - - This sets the :attr:`fixed` indicator to False. - - """ - self.fixed = False - - def free(self): - """Alias for :meth:`unfix`""" - return self.unfix() - - -class _VarData(metaclass=RenamedClass): - __renamed__new_class__ = VarData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralVarData(VarData): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -365,10 +130,6 @@ def copy(cls, src): self._index = src._index return self - # - # Abstract Interface - # - def set_value(self, val, skip_validation=False): """Set the current variable value. @@ -429,14 +190,20 @@ def set_value(self, val, skip_validation=False): @property def value(self): + """Return (or set) the value for this variable.""" return self._value @value.setter def value(self, val): self.set_value(val) + def __call__(self, exception=True): + """Compute the value of this variable.""" + return self._value + @property def domain(self): + """Return (or set) the domain for this variable.""" return self._domain @domain.setter @@ -453,9 +220,42 @@ def domain(self, domain): ) raise - @VarData.bounds.getter + def has_lb(self): + """Returns :const:`False` when the lower bound is + :const:`None` or negative infinity""" + return self.lb is not None + + def has_ub(self): + """Returns :const:`False` when the upper bound is + :const:`None` or positive infinity""" + return self.ub is not None + + # TODO: deprecate this? Properties are generally preferred over "set*()" + def setlb(self, val): + """ + Set the lower bound for this variable after validating that + the value is fixed (or None). + """ + self.lower = val + + # TODO: deprecate this? Properties are generally preferred over "set*()" + def setub(self, val): + """ + Set the upper bound for this variable after validating that + the value is fixed (or None). + """ + self.upper = val + + @property def bounds(self): - # Custom implementation of VarData.bounds to avoid unnecessary + """Returns (or set) the tuple (lower bound, upper bound). + + This returns the current (numeric) values of the lower and upper + bounds as a tuple. If there is no bound, returns None (and not + +/-inf) + + """ + # Custom implementation of lb / ub to avoid unnecessary # expression generation and duplicate calls to domain.bounds() domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -496,10 +296,14 @@ def bounds(self): ub = min(ub, domain_ub) return lb, ub - @VarData.lb.getter + @bounds.setter + def bounds(self, val): + self.lower, self.upper = val + + @property def lb(self): - # Custom implementation of VarData.lb to avoid unnecessary - # expression generation + """Return (or set) the numeric value of the variable lower bound.""" + # Note: Implementation avoids unnecessary expression generation domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds lb = self._lb @@ -521,10 +325,14 @@ def lb(self): lb = max(lb, domain_lb) return lb - @VarData.ub.getter + @lb.setter + def lb(self, val): + self.lower = val + + @property def ub(self): - # Custom implementation of VarData.ub to avoid unnecessary - # expression generation + """Return (or set) the numeric value of the variable upper bound.""" + # Note: implementation avoids unnecessary expression generation domain_lb, domain_ub = self.domain.bounds() # ub is the tighter of the domain and bounds ub = self._ub @@ -546,6 +354,10 @@ def ub(self): ub = min(ub, domain_ub) return ub + @ub.setter + def ub(self, val): + self.upper = val + @property def lower(self): """Return (or set) an expression for the variable lower bound. @@ -602,8 +414,37 @@ def get_units(self): # component if not scalar return self.parent_component()._units + def fix(self, value=NOTSET, skip_validation=False): + """Fix the value of this variable (treat as nonvariable) + + This sets the :attr:`fixed` indicator to True. If ``value`` is + provided, the value (and the ``skip_validation`` flag) are first + passed to :meth:`set_value()`. + + """ + self.fixed = True + if value is not NOTSET: + self.set_value(value, skip_validation) + + def unfix(self): + """Unfix this variable (treat as variable in solver interfaces) + + This sets the :attr:`fixed` indicator to False. + + """ + self.fixed = False + + def free(self): + """Alias for :meth:`unfix`""" + return self.unfix() + @property def fixed(self): + """Return (or set) the fixed indicator for this variable. + + Alias for :meth:`is_fixed` / :meth:`fix` / :meth:`unfix`. + + """ return self._fixed @fixed.setter @@ -612,6 +453,28 @@ def fixed(self, val): @property def stale(self): + """The stale status for this variable. + + Variables are "stale" if their current value was not updated as + part of the most recent model update. A "model update" can be + one of several things: a solver invocation, loading a previous + solution, or manually updating a non-stale :class:`Var` value. + + Returns + ------- + bool + + Notes + ----- + Fixed :class:`Var` objects will be stale after invoking a solver + (as their value was not updated by the solver). + + Updating a stale :class:`Var` value will not cause other + variable values to be come stale. However, updating the first + non-stale :class:`Var` value after a solve or solution load + *will* cause all other variables to be marked as stale + + """ return StaleFlagManager.is_stale(self._stale) @stale.setter @@ -621,11 +484,70 @@ def stale(self, val): else: self._stale = StaleFlagManager.get_flag(0) - # Note: override the base class definition to avoid a call through a - # property + def is_integer(self): + """Returns True when the domain is a contiguous integer range.""" + _id = id(self.domain) + if _id in _known_global_real_domains: + return not _known_global_real_domains[_id] + _interval = self.domain.get_interval() + if _interval is None: + return False + # Note: it is not sufficient to just check the step: the + # starting / ending points must be integers (or not specified) + start, stop, step = _interval + return ( + step == 1 + and (start is None or int(start) == start) + and (stop is None or int(stop) == stop) + ) + + def is_binary(self): + """Returns True when the domain is restricted to Binary values.""" + domain = self.domain + if domain is Binary: + return True + if id(domain) in _known_global_real_domains: + return False + return domain.get_interval() == (0, 1, 1) + + def is_continuous(self): + """Returns True when the domain is a continuous real range""" + _id = id(self.domain) + if _id in _known_global_real_domains: + return _known_global_real_domains[_id] + _interval = self.domain.get_interval() + return _interval is not None and _interval[2] == 0 + def is_fixed(self): + """Returns True if this variable is fixed, otherwise returns False.""" return self._fixed + def is_constant(self): + """Returns False because this is not a constant in an expression.""" + return False + + def is_variable_type(self): + """Returns True because this is a variable.""" + return True + + def is_potentially_variable(self): + """Returns True because this is a variable.""" + return True + + def clear(self): + self.value = None + + def _compute_polynomial_degree(self, result): + """ + If the variable is fixed, it represents a constant + is a polynomial with degree 0. Otherwise, it has + degree 1. This method is used in expressions to + compute polynomial degree. + """ + if self._fixed: + return 0 + return 1 + def _process_bound(self, val, bound_type): if type(val) in native_numeric_types or val is None: # TODO: warn/error: check if this Var has units: assigning @@ -648,8 +570,13 @@ def _process_bound(self, val, bound_type): return val -class _GeneralVarData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralVarData +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData + __renamed__version__ = '6.7.2.dev0' + + +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData __renamed__version__ = '6.7.2.dev0' @@ -678,7 +605,7 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): doc (str, optional): Text describing this component. """ - _ComponentDataClass = GeneralVarData + _ComponentDataClass = VarData @overload def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... @@ -962,11 +889,11 @@ def _pprint(self): ) -class ScalarVar(GeneralVarData, Var): +class ScalarVar(VarData, Var): """A single variable.""" def __init__(self, *args, **kwd): - GeneralVarData.__init__(self, component=self) + VarData.__init__(self, component=self) Var.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -1067,7 +994,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args) -> GeneralVarData: + def __getitem__(self, args) -> VarData: try: return super().__getitem__(args) except RuntimeError: From cf7ff538df92247af0cd7e7c3e34ed4f4549d8af Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:21:36 -0600 Subject: [PATCH 0760/1178] Update references from GeneralVarData to VarData --- pyomo/contrib/appsi/base.py | 60 +++++++++---------- pyomo/contrib/appsi/cmodel/src/expression.hpp | 6 +- pyomo/contrib/appsi/fbbt.py | 10 ++-- pyomo/contrib/appsi/solvers/cbc.py | 16 ++--- pyomo/contrib/appsi/solvers/cplex.py | 16 ++--- pyomo/contrib/appsi/solvers/gurobi.py | 12 ++-- pyomo/contrib/appsi/solvers/highs.py | 8 +-- pyomo/contrib/appsi/solvers/ipopt.py | 16 ++--- pyomo/contrib/appsi/solvers/wntr.py | 8 +-- pyomo/contrib/appsi/writers/lp_writer.py | 8 +-- pyomo/contrib/appsi/writers/nl_writer.py | 8 +-- pyomo/contrib/cp/repn/docplex_writer.py | 4 +- .../logical_to_disjunctive_walker.py | 4 +- pyomo/contrib/latex_printer/latex_printer.py | 12 ++-- pyomo/contrib/parmest/utils/scenario_tree.py | 2 +- pyomo/contrib/solver/base.py | 24 ++++---- pyomo/contrib/solver/gurobi.py | 12 ++-- pyomo/contrib/solver/ipopt.py | 6 +- pyomo/contrib/solver/persistent.py | 18 +++--- pyomo/contrib/solver/solution.py | 26 ++++---- .../contrib/solver/tests/unit/test_results.py | 10 ++-- .../trustregion/tests/test_interface.py | 4 +- pyomo/core/base/__init__.py | 9 ++- pyomo/core/base/component.py | 2 +- pyomo/core/expr/calculus/derivatives.py | 6 +- pyomo/core/tests/transform/test_add_slacks.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 6 +- pyomo/core/tests/unit/test_list_objects.py | 6 +- pyomo/core/tests/unit/test_numeric_expr.py | 4 +- pyomo/core/tests/unit/test_reference.py | 12 ++-- pyomo/repn/standard_repn.py | 6 +- .../plugins/solvers/gurobi_persistent.py | 4 +- pyomo/util/report_scaling.py | 4 +- 33 files changed, 171 insertions(+), 180 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 409c8e2596c..930ff8393e9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -23,7 +23,7 @@ ) from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint -from pyomo.core.base.var import GeneralVarData, Var +from pyomo.core.base.var import VarData, Var from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import GeneralObjectiveData @@ -179,9 +179,7 @@ def __init__( class SolutionLoaderBase(abc.ABC): - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -197,8 +195,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -256,8 +254,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -303,8 +301,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -353,8 +351,8 @@ def get_slacks( return slacks def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' @@ -708,9 +706,7 @@ class PersistentSolver(Solver): def is_persistent(self): return True - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -726,8 +722,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: pass def get_duals( @@ -771,8 +767,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Parameters ---------- @@ -799,7 +795,7 @@ def set_instance(self, model): pass @abc.abstractmethod - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -815,7 +811,7 @@ def add_block(self, block: BlockData): pass @abc.abstractmethod - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -835,7 +831,7 @@ def set_objective(self, obj: GeneralObjectiveData): pass @abc.abstractmethod - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -869,8 +865,8 @@ def get_slacks( return self._solver.get_slacks(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return self._solver.get_reduced_costs(vars_to_load=vars_to_load) @@ -954,10 +950,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): pass - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -987,7 +983,7 @@ def add_params(self, params: List[ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[GeneralVarData]): + def _check_for_new_vars(self, variables: List[VarData]): new_vars = dict() for v in variables: v_id = id(v) @@ -995,7 +991,7 @@ def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[GeneralVarData]): + def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove = dict() for v in variables: v_id = id(v) @@ -1174,10 +1170,10 @@ def remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): pass - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -1246,10 +1242,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): pass - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 0c0777ef468..803bb21b6e2 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -680,7 +680,7 @@ class PyomoExprTypes { expr_type_map[np_float32] = py_float; expr_type_map[np_float64] = py_float; expr_type_map[ScalarVar] = var; - expr_type_map[_GeneralVarData] = var; + expr_type_map[_VarData] = var; expr_type_map[AutoLinkedBinaryVar] = var; expr_type_map[ScalarParam] = param; expr_type_map[_ParamData] = param; @@ -732,8 +732,8 @@ class PyomoExprTypes { py::module_::import("pyomo.core.base.param").attr("_ParamData"); py::object ScalarVar = py::module_::import("pyomo.core.base.var").attr("ScalarVar"); - py::object _GeneralVarData = - py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object _VarData = + py::module_::import("pyomo.core.base.var").attr("_VarData"); py::object AutoLinkedBinaryVar = py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 122ca5f7ffd..1ebb3d40381 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -18,7 +18,7 @@ ) from .cmodel import cmodel, cmodel_available from typing import List, Optional -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData @@ -121,7 +121,7 @@ def set_instance(self, model, symbolic_solver_labels: Optional[bool] = None): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): if self._symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -190,7 +190,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): if self._symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -205,7 +205,7 @@ def _remove_params(self, params: List[ParamData]): for p in params: del self._param_map[id(p)] - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._pyomo_expr_types, variables, @@ -304,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: BlockData, seed_var: GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: VarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index d03e6e31c54..7db9a32764e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -26,7 +26,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData @@ -164,7 +164,7 @@ def symbol_map(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) def add_params(self, params: List[ParamData]): @@ -176,7 +176,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[ParamData]): @@ -191,7 +191,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -440,8 +440,8 @@ def _check_and_escape_options(): return results def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -477,8 +477,8 @@ def get_duals(self, cons_to_load=None): return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 55259244d45..0ed3495ac1c 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -22,7 +22,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData @@ -179,7 +179,7 @@ def update_config(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) def add_params(self, params: List[ParamData]): @@ -191,7 +191,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[ParamData]): @@ -206,7 +206,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -362,8 +362,8 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): return results def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none @@ -440,8 +440,8 @@ def get_duals( return res def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 8606d44cd46..e2ecd9b69e7 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, GeneralVarData +from pyomo.core.base.var import Var, VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData @@ -458,7 +458,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): var_names = list() vtypes = list() lbs = list() @@ -759,7 +759,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -774,7 +774,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): def _remove_params(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1221,7 +1221,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1256,7 +1256,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 5af7b297684..c3083ac78d3 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,7 +20,7 @@ from pyomo.common.log import LogStream from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData @@ -308,7 +308,7 @@ def _process_domain_and_bounds(self, var_id): return lb, ub, vtype - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -493,7 +493,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -518,7 +518,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): def _remove_params(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index ca75a1b02c8..5cd9a51785d 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -28,7 +28,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData @@ -228,7 +228,7 @@ def set_instance(self, model): self._writer.config.symbolic_solver_labels = self.config.symbolic_solver_labels self._writer.set_instance(model) - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) def add_params(self, params: List[ParamData]): @@ -240,7 +240,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[ParamData]): @@ -255,7 +255,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -514,8 +514,8 @@ def _apply_solver(self, timer: HierarchicalTimer): return results def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -551,8 +551,8 @@ def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = No return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 8f2650dabb6..62c4b0ed358 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -40,7 +40,7 @@ from pyomo.core.expr.numvalue import native_numeric_types from typing import Dict, Optional, List from pyomo.core.base.block import BlockData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer @@ -239,7 +239,7 @@ def set_instance(self, model): self.add_block(model) - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): aml = wntr.sim.aml.aml for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) @@ -302,7 +302,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): del self._pyomo_con_to_solver_con_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) solver_var = self._pyomo_var_to_solver_var_map[v_id] @@ -322,7 +322,7 @@ def _remove_params(self, params: List[ParamData]): self._symbol_map.removeSymbol(p) del self._pyomo_param_to_solver_param_map[p_id] - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): aml = wntr.sim.aml.aml for var in variables: v_id = id(var) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 3a6682d5c00..4be2b32d83d 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import ParamData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import SOSConstraintData @@ -77,7 +77,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -117,7 +117,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for v in variables: cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] @@ -128,7 +128,7 @@ def _remove_params(self, params: List[ParamData]): del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index c46cb1c0723..70176146a1e 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import ParamData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import SOSConstraintData @@ -78,7 +78,7 @@ def set_instance(self, model): self.set_objective(None) self._set_pyomo_amplfunc_env() - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): if self.config.symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -144,7 +144,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): if self.config.symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -161,7 +161,7 @@ def _remove_params(self, params: List[ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 75095755895..221fd61af5b 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -65,7 +65,7 @@ ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData -from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, VarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet @@ -961,7 +961,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, ScalarVar: _before_var, - GeneralVarData: _before_var, + VarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, GeneralBooleanVarData: _before_boolean_var, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index a228b1561dd..d9483c0ed14 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -29,7 +29,7 @@ import pyomo.core.base.boolean_var as BV from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import ScalarParam, ParamData -from pyomo.core.base.var import ScalarVar, GeneralVarData +from pyomo.core.base.var import ScalarVar, VarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -216,7 +216,7 @@ def _dispatch_atmost(visitor, node, *args): # for the moment, these are all just so we can get good error messages when we # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var -_before_child_dispatcher[GeneralVarData] = _dispatch_var +_before_child_dispatcher[VarData] = _dispatch_var _before_child_dispatcher[GeneralExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 28d1ca52943..e11543cb375 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -47,7 +47,7 @@ resolve_template, templatize_rule, ) -from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, VarData, IndexedVar from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam from pyomo.core.base.set import SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint @@ -404,7 +404,7 @@ def __init__(self): kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, GeneralObjectiveData: handle_named_expression_node, - GeneralVarData: handle_var_node, + VarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, ExternalFunctionExpression: handle_external_function_node, @@ -705,10 +705,8 @@ def latex_printer( if isSingle: temp_comp, temp_indexes = templatize_fcn(pyomo_component) variableList = [] - for v in identify_components( - temp_comp, [ScalarVar, GeneralVarData, IndexedVar] - ): - if isinstance(v, GeneralVarData): + for v in identify_components(temp_comp, [ScalarVar, VarData, IndexedVar]): + if isinstance(v, VarData): v_write = v.parent_component() if v_write not in ComponentSet(variableList): variableList.append(v_write) @@ -1273,7 +1271,7 @@ def get_index_names(st, lcm): rep_dict = {} for ky in reversed(list(latex_component_map)): - if isinstance(ky, (pyo.Var, GeneralVarData)): + if isinstance(ky, (pyo.Var, VarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index 1062e4a2bf4..f245e053cad 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -25,7 +25,7 @@ def build_vardatalist(self, model, varlist=None): """ - Convert a list of pyomo variables to a list of ScalarVar and GeneralVarData. If varlist is none, builds a + Convert a list of pyomo variables to a list of ScalarVar and VarData. If varlist is none, builds a list of all variables in the model. The new list is stored in the vars_to_tighten attribute. By CD Laird Parameters diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 1b22c17cf48..fdc7361e6b8 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -15,7 +15,7 @@ import os from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData from pyomo.core.base.objective import GeneralObjectiveData @@ -194,9 +194,7 @@ def is_persistent(self): """ return True - def _load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def _load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -212,19 +210,19 @@ def _load_vars( @abc.abstractmethod def _get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Get mapping of variables to primals. Parameters ---------- - vars_to_load : Optional[Sequence[GeneralVarData]], optional + vars_to_load : Optional[Sequence[VarData]], optional Which vars to be populated into the map. The default is None. Returns ------- - Mapping[GeneralVarData, float] + Mapping[VarData, float] A map of variables to primals. """ raise NotImplementedError( @@ -251,8 +249,8 @@ def _get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def _get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Parameters ---------- @@ -282,7 +280,7 @@ def set_objective(self, obj: GeneralObjectiveData): """ @abc.abstractmethod - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): """ Add variables to the model """ @@ -306,7 +304,7 @@ def add_block(self, block: BlockData): """ @abc.abstractmethod - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): """ Remove variables from the model """ @@ -330,7 +328,7 @@ def remove_block(self, block: BlockData): """ @abc.abstractmethod - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): """ Update variables on the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index c5a1c8c1cd8..ff4e93f7635 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -22,7 +22,7 @@ from pyomo.common.config import ConfigValue from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData @@ -438,7 +438,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): var_names = list() vtypes = list() lbs = list() @@ -735,7 +735,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -750,7 +750,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): def _remove_parameters(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1151,7 +1151,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1186,7 +1186,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 7111ec6e972..e4d25e4fea0 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -25,7 +25,7 @@ ) from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase @@ -80,8 +80,8 @@ def __init__( class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index e98d76b4841..103eb3c622f 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -14,7 +14,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap @@ -54,10 +54,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): pass - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -87,7 +87,7 @@ def add_parameters(self, params: List[ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[GeneralVarData]): + def _check_for_new_vars(self, variables: List[VarData]): new_vars = {} for v in variables: v_id = id(v) @@ -95,7 +95,7 @@ def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[GeneralVarData]): + def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove = {} for v in variables: v_id = id(v) @@ -250,10 +250,10 @@ def remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): pass - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -309,10 +309,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): pass - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 3f327c1f280..e089e621f1f 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -13,7 +13,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap from pyomo.common.errors import DeveloperError @@ -30,9 +30,7 @@ class SolutionLoaderBase(abc.ABC): Intent of this class and its children is to load the solution back into the model. """ - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -49,8 +47,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -86,8 +84,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -127,8 +125,8 @@ def get_duals( return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return self._solver._get_reduced_costs(vars_to_load=vars_to_load) @@ -141,9 +139,7 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: self._sol_data = sol_data self._nl_info = nl_info - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -169,8 +165,8 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 608af04a0ed..6c178d80298 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -16,7 +16,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results from pyomo.contrib.solver import solution @@ -51,8 +51,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -84,8 +84,8 @@ def get_duals( return duals def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index 64f76eb887d..0922ccf950b 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -33,7 +33,7 @@ cos, SolverFactory, ) -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.expr.numeric_expr import ExternalFunctionExpression from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.trustregion.interface import TRFInterface @@ -158,7 +158,7 @@ def test_replaceExternalFunctionsWithVariables(self): self.assertIsInstance(k, ExternalFunctionExpression) self.assertIn(str(self.interface.model.x[0]), str(k)) self.assertIn(str(self.interface.model.x[1]), str(k)) - self.assertIsInstance(i, GeneralVarData) + self.assertIsInstance(i, VarData) self.assertEqual(i, self.interface.data.ef_outputs[1]) for i, k in self.interface.data.basis_expressions.items(): self.assertEqual(k, 0) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 9a06a3e02bb..6851fe4cda0 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -40,7 +40,6 @@ from pyomo.core.base.componentuid import ComponentUID from pyomo.core.base.config import PyomoOptions from pyomo.core.base.enums import SortComponents, TraversalStrategy -from pyomo.core.base.instance2dat import instance2dat from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, @@ -146,7 +145,9 @@ active_import_suffix_generator, Suffix, ) -from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList +from pyomo.core.base.var import Var, VarData, VarData, ScalarVar, VarList + +from pyomo.core.base.instance2dat import instance2dat # # These APIs are deprecated and should be removed in the near future @@ -163,12 +164,14 @@ 'SimpleBooleanVar', 'pyomo.core.base.boolean_var.SimpleBooleanVar', version='6.0' ) # Historically, only a subset of "private" component data classes were imported here +relocated_module_attribute( + f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2.dev0' +) for _cdata in ( 'ConstraintData', 'LogicalConstraintData', 'ExpressionData', 'VarData', - 'GeneralVarData', 'GeneralBooleanVarData', 'BooleanVarData', 'ObjectiveData', diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 7a4b7e40aab..65844379eca 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, + # ParamData,VarData, GeneralBooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index 5df1fd3c65e..69fe4969938 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -39,11 +39,11 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression The expression to differentiate - wrt: pyomo.core.base.var.GeneralVarData + wrt: pyomo.core.base.var.VarData If specified, this function will return the derivative with - respect to wrt. wrt is normally a GeneralVarData, but could + respect to wrt. wrt is normally a VarData, but could also be a ParamData. wrt and wrt_list cannot both be specified. - wrt_list: list of pyomo.core.base.var.GeneralVarData + wrt_list: list of pyomo.core.base.var.VarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned where the values are the derivatives with respect to the diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index d66d6fba79e..b395237b8e4 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -330,7 +330,7 @@ def test_error_for_non_constraint_noniterable_target(self): self.assertRaisesRegex( ValueError, "Expected Constraint or list of Constraints.\n\tReceived " - "", + "", TransformationFactory('core.add_slack_variables').apply_to, m, targets=m.indexedVar[1], diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index f2c3cad8cc3..0dc5cacd216 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -17,7 +17,7 @@ ObjectiveDict, ExpressionDict, ) -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -348,10 +348,10 @@ def test_active(self): class TestVarDict(_TestComponentDictBase, unittest.TestCase): - # Note: the updated GeneralVarData class only takes an optional + # Note: the updated VarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = VarDict - _cdatatype = lambda self, arg: GeneralVarData() + _cdatatype = lambda self, arg: VarData() def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index fcc83a95a06..f98b5279fc5 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -17,7 +17,7 @@ XObjectiveList, XExpressionList, ) -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -365,10 +365,10 @@ def test_active(self): class TestVarList(_TestComponentListBase, unittest.TestCase): - # Note: the updated GeneralVarData class only takes an optional + # Note: the updated VarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = XVarList - _cdatatype = lambda self, arg: GeneralVarData() + _cdatatype = lambda self, arg: VarData() def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 8e5e43eac9c..efb01e6d6ce 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -112,7 +112,7 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr import expr_common -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.repn import generate_standard_repn from pyomo.core.expr.numvalue import NumericValue @@ -294,7 +294,7 @@ def value_check(self, exp, val): class TestExpression_EvaluateVarData(TestExpression_EvaluateNumericValue): def create(self, val, domain): - tmp = GeneralVarData() + tmp = VarData() tmp.domain = domain tmp.value = val return tmp diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index 4fa2f4944e9..7370881612f 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -800,8 +800,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Object - 1 : - 2 : + 1 : + 2 : """, ) m.s = Reference(m.x[:, ...], ctype=IndexedComponent) @@ -811,8 +811,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Object - 1 : - 2 : + 1 : + 2 : """, ) @@ -1357,8 +1357,8 @@ def test_pprint_nonfinite_sets_ctypeNone(self): 1 IndexedComponent Declarations ref : Size=2, Index=NonNegativeIntegers, ReferenceTo=v Key : Object - 3 : - 5 : + 3 : + 5 : 2 Declarations: v ref """.strip(), diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 5786d078385..2f5a413e963 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -22,7 +22,7 @@ from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.var import ScalarVar, Var, GeneralVarData, value +from pyomo.core.base.var import ScalarVar, Var, VarData, value from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable @@ -1143,7 +1143,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra # param.Param : _collect_linear_const, # parameter : _collect_linear_const, NumericConstant: _collect_const, - GeneralVarData: _collect_var, + VarData: _collect_var, ScalarVar: _collect_var, Var: _collect_var, variable: _collect_var, @@ -1542,7 +1542,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, - GeneralVarData : _linear_collect_var, + VarData : _linear_collect_var, ScalarVar : _linear_collect_var, Var : _linear_collect_var, variable : _linear_collect_var, diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 97a3533c3f9..17ce33fd95f 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -192,7 +192,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - con: pyomo.core.base.var.GeneralVarData + con: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -342,7 +342,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 265564bf12d..7619662c482 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -13,7 +13,7 @@ import math from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, GeneralVarData): + if isinstance(_v, VarData): if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) From 458c84e5acabcb95186c780238ec7f951b4e54b4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:29:24 -0600 Subject: [PATCH 0761/1178] Merge GeneralBooleanVarData into BooelanVarData class --- pyomo/core/base/boolean_var.py | 205 +++++++++++++-------------------- 1 file changed, 78 insertions(+), 127 deletions(-) diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 925dca530a7..b6a25cd8f27 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -68,27 +68,61 @@ def __setstate__(self, state): self._boolvar = weakref_ref(state) +def _associated_binary_mapper(encode, val): + if val is None: + return None + if encode: + if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: + return val() + else: + if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: + return weakref_ref(val) + return val + + class BooleanVarData(ComponentData, BooleanValue): """ - This class defines the data for a single variable. + This class defines the data for a single Boolean variable. Constructor Arguments: component The BooleanVar object that owns this data. + Public Class Attributes: + domain The domain of this variable. fixed If True, then this variable is treated as a fixed constant in the model. stale A Boolean indicating whether the value of this variable is - legitimate. This value is true if the value should + legitimiate. This value is true if the value should be considered legitimate for purposes of reporting or other interrogation. value The numeric value of this variable. + + The domain attribute is a property because it is + too widely accessed directly to enforce explicit getter/setter + methods and we need to deter directly modifying or accessing + these attributes in certain cases. """ - __slots__ = () + __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') + __autoslot_mappers__ = { + '_associated_binary': _associated_binary_mapper, + '_stale': StaleFlagManager.stale_mapper, + } def __init__(self, component=None): + # + # These lines represent in-lining of the + # following constructors: + # - BooleanVarData + # - ComponentData + # - BooleanValue self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET + self._value = None + self.fixed = False + self._stale = 0 # True + + self._associated_binary = None def is_fixed(self): """Returns True if this variable is fixed, otherwise returns False.""" @@ -132,118 +166,6 @@ def __call__(self, exception=True): """Compute the value of this variable.""" return self.value - @property - def value(self): - """Return the value for this variable.""" - raise NotImplementedError - - @property - def domain(self): - """Return the domain for this variable.""" - raise NotImplementedError - - @property - def fixed(self): - """Return the fixed indicator for this variable.""" - raise NotImplementedError - - @property - def stale(self): - """Return the stale indicator for this variable.""" - raise NotImplementedError - - def fix(self, value=NOTSET, skip_validation=False): - """Fix the value of this variable (treat as nonvariable) - - This sets the `fixed` indicator to True. If ``value`` is - provided, the value (and the ``skip_validation`` flag) are first - passed to :py:meth:`set_value()`. - - """ - self.fixed = True - if value is not NOTSET: - self.set_value(value, skip_validation) - - def unfix(self): - """Unfix this variable (treat as variable) - - This sets the `fixed` indicator to False. - - """ - self.fixed = False - - def free(self): - """Alias for :py:meth:`unfix`""" - return self.unfix() - - -class _BooleanVarData(metaclass=RenamedClass): - __renamed__new_class__ = BooleanVarData - __renamed__version__ = '6.7.2.dev0' - - -def _associated_binary_mapper(encode, val): - if val is None: - return None - if encode: - if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: - return val() - else: - if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: - return weakref_ref(val) - return val - - -class GeneralBooleanVarData(BooleanVarData): - """ - This class defines the data for a single Boolean variable. - - Constructor Arguments: - component The BooleanVar object that owns this data. - - Public Class Attributes: - domain The domain of this variable. - fixed If True, then this variable is treated as a - fixed constant in the model. - stale A Boolean indicating whether the value of this variable is - legitimiate. This value is true if the value should - be considered legitimate for purposes of reporting or - other interrogation. - value The numeric value of this variable. - - The domain attribute is a property because it is - too widely accessed directly to enforce explicit getter/setter - methods and we need to deter directly modifying or accessing - these attributes in certain cases. - """ - - __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') - __autoslot_mappers__ = { - '_associated_binary': _associated_binary_mapper, - '_stale': StaleFlagManager.stale_mapper, - } - - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - BooleanVarData - # - ComponentData - # - BooleanValue - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._value = None - self.fixed = False - self._stale = 0 # True - - self._associated_binary = None - - # - # Abstract Interface - # - - # value is an attribute - @property def value(self): """Return (or set) the value for this variable.""" @@ -271,13 +193,13 @@ def stale(self, val): def get_associated_binary(self): """Get the binary VarData associated with this - GeneralBooleanVarData""" + BooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary VarData to this GeneralBooleanVarData""" + """Associate a binary VarData to this BooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) @@ -299,9 +221,38 @@ def associate_binary_var(self, binary_var): if binary_var is not None: self._associated_binary = weakref_ref(binary_var) + def fix(self, value=NOTSET, skip_validation=False): + """Fix the value of this variable (treat as nonvariable) + + This sets the `fixed` indicator to True. If ``value`` is + provided, the value (and the ``skip_validation`` flag) are first + passed to :py:meth:`set_value()`. + + """ + self.fixed = True + if value is not NOTSET: + self.set_value(value, skip_validation) + + def unfix(self): + """Unfix this variable (treat as variable) + + This sets the `fixed` indicator to False. -class _GeneralBooleanVarData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralBooleanVarData + """ + self.fixed = False + + def free(self): + """Alias for :py:meth:`unfix`""" + return self.unfix() + + +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData + __renamed__version__ = '6.7.2.dev0' + + +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData __renamed__version__ = '6.7.2.dev0' @@ -319,7 +270,7 @@ class BooleanVar(IndexedComponent): to True. """ - _ComponentDataClass = GeneralBooleanVarData + _ComponentDataClass = BooleanVarData def __new__(cls, *args, **kwds): if cls != BooleanVar: @@ -511,11 +462,11 @@ def _pprint(self): ) -class ScalarBooleanVar(GeneralBooleanVarData, BooleanVar): +class ScalarBooleanVar(BooleanVarData, BooleanVar): """A single variable.""" def __init__(self, *args, **kwd): - GeneralBooleanVarData.__init__(self, component=self) + BooleanVarData.__init__(self, component=self) BooleanVar.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -531,7 +482,7 @@ def __init__(self, *args, **kwd): def value(self): """Return the value for this variable.""" if self._constructed: - return GeneralBooleanVarData.value.fget(self) + return BooleanVarData.value.fget(self) raise ValueError( "Accessing the value of variable '%s' " "before the Var has been constructed (there " @@ -542,7 +493,7 @@ def value(self): def value(self, val): """Set the value for this variable.""" if self._constructed: - return GeneralBooleanVarData.value.fset(self, val) + return BooleanVarData.value.fset(self, val) raise ValueError( "Setting the value of variable '%s' " "before the Var has been constructed (there " @@ -551,7 +502,7 @@ def value(self, val): @property def domain(self): - return GeneralBooleanVarData.domain.fget(self) + return BooleanVarData.domain.fget(self) def fix(self, value=NOTSET, skip_validation=False): """ @@ -559,7 +510,7 @@ def fix(self, value=NOTSET, skip_validation=False): indicating the variable should be fixed at its current value. """ if self._constructed: - return GeneralBooleanVarData.fix(self, value, skip_validation) + return BooleanVarData.fix(self, value, skip_validation) raise ValueError( "Fixing variable '%s' " "before the Var has been constructed (there " @@ -569,7 +520,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Sets the fixed indicator to False.""" if self._constructed: - return GeneralBooleanVarData.unfix(self) + return BooleanVarData.unfix(self) raise ValueError( "Freeing variable '%s' " "before the Var has been constructed (there " From 015a4f859d63d9cdf615a488490301f88a2c96cc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:30:30 -0600 Subject: [PATCH 0762/1178] Update references from GeneralBooleanVarData to BooelanVarData --- pyomo/contrib/cp/repn/docplex_writer.py | 4 ++-- pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py | 2 +- pyomo/core/base/__init__.py | 6 ++++-- pyomo/core/base/boolean_var.py | 2 +- pyomo/core/base/component.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 221fd61af5b..1af153910c0 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -60,7 +60,7 @@ ) from pyomo.core.base.boolean_var import ( ScalarBooleanVar, - GeneralBooleanVarData, + BooleanVarData, IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData @@ -964,7 +964,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): VarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, - GeneralBooleanVarData: _before_boolean_var, + BooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index d9483c0ed14..0c493b89321 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -209,7 +209,7 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher = {} _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[BV.GeneralBooleanVarData] = _dispatch_boolean_var +_before_child_dispatcher[BV.BooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var _before_child_dispatcher[ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 6851fe4cda0..bcc2a0e0e02 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -84,7 +84,7 @@ from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, - GeneralBooleanVarData, + BooleanVarData, BooleanVarList, ScalarBooleanVar, ) @@ -167,12 +167,14 @@ relocated_module_attribute( f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2.dev0' ) +relocated_module_attribute( + f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2.dev0' +) for _cdata in ( 'ConstraintData', 'LogicalConstraintData', 'ExpressionData', 'VarData', - 'GeneralBooleanVarData', 'BooleanVarData', 'ObjectiveData', ): diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index b6a25cd8f27..98761dee536 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -251,7 +251,7 @@ class _BooleanVarData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _BooleanVarData(metaclass=RenamedClass): +class _GeneralBooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData __renamed__version__ = '6.7.2.dev0' diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 65844379eca..1c73809a25a 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # ParamData,VarData, GeneralBooleanVarData, DisjunctionData, + # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! From 0cbfcb7418310b2dbb97aa6e915d4570e02c1d37 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:38:00 -0600 Subject: [PATCH 0763/1178] Restore deprecation path for GeneralVarData --- pyomo/core/base/var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index b1634b61c44..8870fc5b09c 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -575,7 +575,7 @@ class _VarData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _VarData(metaclass=RenamedClass): +class _GeneralVarData(metaclass=RenamedClass): __renamed__new_class__ = VarData __renamed__version__ = '6.7.2.dev0' From adfe72b6ff4a6900f98d77dad7cac83da7e41250 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:38:35 -0600 Subject: [PATCH 0764/1178] Merge GeneralLogicalConstraintData into LogicalConstratintData --- pyomo/core/base/component.py | 2 +- pyomo/core/base/logical_constraint.py | 77 ++++----------------------- 2 files changed, 11 insertions(+), 68 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 1c73809a25a..3dd4d47046d 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, - # GeneralLogicalConstraintData, GeneralObjectiveData, + # LogicalConstraintData, GeneralObjectiveData, # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 23a422705df..1daa5f83e90 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -43,68 +43,6 @@ class LogicalConstraintData(ActiveComponentData): - """ - This class defines the data for a single logical constraint. - - It functions as a pure interface. - - Constructor arguments: - component The LogicalConstraint object that owns this data. - - Public class attributes: - active A boolean that is true if this statement is - active in the model. - body The Pyomo logical expression for this statement - - Private class attributes: - _component The statement component. - _active A boolean that indicates whether this data is active - """ - - __slots__ = () - - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - ActiveComponentData - # - ComponentData - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._active = True - - # - # Interface - # - def __call__(self, exception=True): - """Compute the value of the body of this logical constraint.""" - if self.body is None: - return None - return self.body(exception=exception) - - # - # Abstract Interface - # - @property - def expr(self): - """Get the expression on this logical constraint.""" - raise NotImplementedError - - def set_value(self, expr): - """Set the expression on this logical constraint.""" - raise NotImplementedError - - def get_value(self): - """Get the expression on this logical constraint.""" - raise NotImplementedError - - -class _LogicalConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = LogicalConstraintData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralLogicalConstraintData(LogicalConstraintData): """ This class defines the data for a single general logical constraint. @@ -178,8 +116,13 @@ def get_value(self): return self._expr +class _LogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = LogicalConstraintData + __renamed__version__ = '6.7.2.dev0' + + class _GeneralLogicalConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralLogicalConstraintData + __renamed__new_class__ = LogicalConstraintData __renamed__version__ = '6.7.2.dev0' @@ -225,7 +168,7 @@ class LogicalConstraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = GeneralLogicalConstraintData + _ComponentDataClass = LogicalConstraintData class Infeasible(object): pass @@ -419,14 +362,14 @@ def _check_skip_add(self, index, expr): return expr -class ScalarLogicalConstraint(GeneralLogicalConstraintData, LogicalConstraint): +class ScalarLogicalConstraint(LogicalConstraintData, LogicalConstraint): """ ScalarLogicalConstraint is the implementation representing a single, non-indexed logical constraint. """ def __init__(self, *args, **kwds): - GeneralLogicalConstraintData.__init__(self, component=self, expr=None) + LogicalConstraintData.__init__(self, component=self, expr=None) LogicalConstraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -446,7 +389,7 @@ def body(self): "an expression. There is currently " "nothing to access." % self.name ) - return GeneralLogicalConstraintData.body.fget(self) + return LogicalConstraintData.body.fget(self) raise ValueError( "Accessing the body of logical constraint '%s' " "before the LogicalConstraint has been constructed (there " From 2f1d4a0387995b741028f96e0e05aadc8c4a30a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:44:43 -0600 Subject: [PATCH 0765/1178] NFC: apply black --- pyomo/repn/tests/test_standard_form.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index 9dee2b1d25d..591703e6ae8 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -245,33 +245,21 @@ def test_alternative_forms(self): m, mixed_form=True, column_order=col_order ) - self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]) self.assertEqual( - list(map(str, repn.x)), - ['x', 'y[0]', 'y[1]', 'y[3]'], + repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)] ) + self.assertEqual(list(map(str, repn.x)), ['x', 'y[0]', 'y[1]', 'y[3]']) self.assertEqual( - list(v.bounds for v in repn.x), - [(None, None), (0, 10), (-5, 10), (-5, -2)], + list(v.bounds for v in repn.x), [(None, None), (0, 10), (-5, 10), (-5, -2)] ) ref = np.array( - [ - [1, 0, 2, 0], - [0, 0, 1, 4], - [0, 1, 6, 0], - [0, 1, 6, 0], - [1, 1, 0, 0], - ] + [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] ) self.assertTrue(np.all(repn.A == ref)) print(repn) print(repn.b) self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) - self.assertTrue( - np.all( - repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]) - ) - ) + self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) # Note that the solution is a mix of inequality and equality constraints # self._verify_solution(soln, repn, False) From a27b8791f864546444824e4d803c86e48b6de606 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 21:33:32 -0600 Subject: [PATCH 0766/1178] Merge GeneralObjectiveData into ObjectiveData --- pyomo/core/base/objective.py | 58 ++++++++---------------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 58cb198e1ae..e4956748b6c 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -81,51 +81,8 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -# -# This class is a pure interface -# - - -class ObjectiveData(ExpressionData): - """ - This class defines the data for a single objective. - - Public class attributes: - expr The Pyomo expression for this objective - sense The direction for this objective. - """ - - __slots__ = () - - # - # Interface - # - - def is_minimizing(self): - """Return True if this is a minimization objective.""" - return self.sense == minimize - - # - # Abstract Interface - # - - @property - def sense(self): - """Access sense (direction) of this objective.""" - raise NotImplementedError - - def set_sense(self, sense): - """Set the sense (direction) of this objective.""" - raise NotImplementedError - - -class _ObjectiveData(metaclass=RenamedClass): - __renamed__new_class__ = ObjectiveData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralObjectiveData( - GeneralExpressionDataImpl, ObjectiveData, ActiveComponentData +class ObjectiveData( + GeneralExpressionDataImpl, ActiveComponentData ): """ This class defines the data for a single objective. @@ -166,6 +123,10 @@ def __init__(self, expr=None, sense=minimize, component=None): "value: %s'" % (minimize, maximize, sense) ) + def is_minimizing(self): + """Return True if this is a minimization objective.""" + return self.sense == minimize + def set_value(self, expr): if expr is None: raise ValueError(_rule_returned_none_error % (self.name,)) @@ -197,8 +158,13 @@ def set_sense(self, sense): ) +class _ObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = ObjectiveData + __renamed__version__ = '6.7.2.dev0' + + class _GeneralObjectiveData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralObjectiveData + __renamed__new_class__ = ObjectiveData __renamed__version__ = '6.7.2.dev0' From 13c11e58a8b49de727e3a9d0eeb630b55a19f1be Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 21:38:00 -0600 Subject: [PATCH 0767/1178] Update references from GeneralObjectiveData to ObjectiveData --- pyomo/contrib/appsi/base.py | 8 ++++---- pyomo/contrib/appsi/fbbt.py | 6 +++--- pyomo/contrib/appsi/solvers/cbc.py | 4 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- pyomo/contrib/appsi/writers/lp_writer.py | 4 ++-- pyomo/contrib/appsi/writers/nl_writer.py | 4 ++-- pyomo/contrib/community_detection/detection.py | 4 ++-- pyomo/contrib/latex_printer/latex_printer.py | 4 ++-- pyomo/contrib/solver/base.py | 4 ++-- pyomo/contrib/solver/persistent.py | 6 +++--- pyomo/core/base/component.py | 2 +- pyomo/core/base/objective.py | 16 +++++++--------- pyomo/core/tests/unit/test_dict_objects.py | 4 ++-- pyomo/core/tests/unit/test_list_objects.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 6 +----- pyomo/repn/standard_repn.py | 6 +++--- 17 files changed, 42 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 930ff8393e9..9d00a56e8b9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -26,7 +26,7 @@ from pyomo.core.base.var import VarData, Var from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs @@ -827,7 +827,7 @@ def remove_block(self, block: BlockData): pass @abc.abstractmethod - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): pass @abc.abstractmethod @@ -1050,10 +1050,10 @@ def add_sos_constraints(self, cons: List[SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): pass - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 1ebb3d40381..4b0d6d4876c 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -22,7 +22,7 @@ from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData -from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize +from pyomo.core.base.objective import ObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -224,13 +224,13 @@ def update_params(self): cp = self._param_map[p_id] cp.value = p.value - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._symbolic_solver_labels: if self._objective is not None: self._symbol_map.removeSymbol(self._objective) super().set_objective(obj) - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): if obj is None: ce = cmodel.Constant(0) sense = 0 diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 7db9a32764e..dffd479a5c7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -30,7 +30,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -188,7 +188,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[VarData]): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 0ed3495ac1c..22c11bdfbe8 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer import sys import time @@ -203,7 +203,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[VarData]): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 5cd9a51785d..4144fbbecd9 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -32,7 +32,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -252,7 +252,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[VarData]): diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 4be2b32d83d..3a6193bd314 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -147,7 +147,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): cobj = cmodel.process_lp_objective( self._expr_types, obj, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 70176146a1e..754bd179497 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -180,7 +180,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): if obj is None: const = cmodel.Constant(0) lin_vars = list() diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index af87fa5eb8b..0e2c3912e06 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -31,7 +31,7 @@ Objective, ConstraintList, ) -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.expr.visitor import replace_expressions, identify_variables from pyomo.contrib.community_detection.community_graph import generate_model_graph from pyomo.common.dependencies import networkx as nx @@ -750,7 +750,7 @@ def generate_structured_model(self): # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives # grouped together) if self.with_objective and isinstance( - stored_constraint, (GeneralObjectiveData, Objective) + stored_constraint, (ObjectiveData, Objective) ): # If the constraint is actually an objective, we add it to the block as an objective new_objective = Objective( diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index e11543cb375..13f30f899e4 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -35,7 +35,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.objective import ScalarObjective, GeneralObjectiveData +from pyomo.core.base.objective import ScalarObjective, ObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( GetItemExpression, @@ -403,7 +403,7 @@ def __init__(self): ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, - GeneralObjectiveData: handle_named_expression_node, + ObjectiveData: handle_named_expression_node, VarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index fdc7361e6b8..c53f917bc2a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -18,7 +18,7 @@ from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning @@ -274,7 +274,7 @@ def set_instance(self, model): """ @abc.abstractmethod - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): """ Set current objective for the model """ diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 103eb3c622f..81d0df1334f 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -16,7 +16,7 @@ from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData, Param -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant @@ -149,10 +149,10 @@ def add_sos_constraints(self, cons: List[SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): pass - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 3dd4d47046d..380aab23cbe 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, - # LogicalConstraintData, GeneralObjectiveData, + # LogicalConstraintData, ObjectiveData, # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index e4956748b6c..fea356229fb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -81,9 +81,7 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -class ObjectiveData( - GeneralExpressionDataImpl, ActiveComponentData -): +class ObjectiveData(GeneralExpressionDataImpl, ActiveComponentData): """ This class defines the data for a single objective. @@ -216,7 +214,7 @@ class Objective(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = GeneralObjectiveData + _ComponentDataClass = ObjectiveData NoObjective = ActiveIndexedComponent.Skip def __new__(cls, *args, **kwds): @@ -365,14 +363,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarObjective(GeneralObjectiveData, Objective): +class ScalarObjective(ObjectiveData, Objective): """ ScalarObjective is the implementation representing a single, non-indexed objective. """ def __init__(self, *args, **kwd): - GeneralObjectiveData.__init__(self, expr=None, component=self) + ObjectiveData.__init__(self, expr=None, component=self) Objective.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -408,7 +406,7 @@ def expr(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return GeneralObjectiveData.expr.fget(self) + return ObjectiveData.expr.fget(self) raise ValueError( "Accessing the expression of objective '%s' " "before the Objective has been constructed (there " @@ -431,7 +429,7 @@ def sense(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return GeneralObjectiveData.sense.fget(self) + return ObjectiveData.sense.fget(self) raise ValueError( "Accessing the sense of objective '%s' " "before the Objective has been constructed (there " @@ -474,7 +472,7 @@ def set_sense(self, sense): if self._constructed: if len(self._data) == 0: self._data[None] = self - return GeneralObjectiveData.set_sense(self, sense) + return ObjectiveData.set_sense(self, sense) raise ValueError( "Setting the sense of objective '%s' " "before the Objective has been constructed (there " diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 0dc5cacd216..fae1d21a87e 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -384,7 +384,7 @@ def setUp(self): class TestObjectiveDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ObjectiveDict - _cdatatype = GeneralObjectiveData + _cdatatype = ObjectiveData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index f98b5279fc5..32f2fa328cf 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -401,7 +401,7 @@ def setUp(self): class TestObjectiveList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XObjectiveList - _cdatatype = GeneralObjectiveData + _cdatatype = ObjectiveData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 23e14104b89..3c3c8539294 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -71,11 +71,7 @@ from pyomo.core.base.component import ActiveComponent from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.objective import ( - ScalarObjective, - GeneralObjectiveData, - ObjectiveData, -) +from pyomo.core.base.objective import ScalarObjective, ObjectiveData from pyomo.core.base.suffix import SuffixFinder from pyomo.core.base.var import VarData import pyomo.core.kernel as kernel diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 2f5a413e963..a23ebf6bb4f 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -19,7 +19,7 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant -from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective +from pyomo.core.base.objective import ObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, VarData, value @@ -1154,7 +1154,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra noclone: _collect_identity, ExpressionData: _collect_identity, Expression: _collect_identity, - GeneralObjectiveData: _collect_identity, + ObjectiveData: _collect_identity, ScalarObjective: _collect_identity, objective: _collect_identity, } @@ -1553,7 +1553,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): noclone : _linear_collect_identity, ExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, - GeneralObjectiveData : _linear_collect_identity, + ObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, objective : _linear_collect_identity, } From 787eaf02a59cacd723c866c8f67740a82888c14b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 22:38:41 -0600 Subject: [PATCH 0768/1178] Rename _ExpressionData, _GeneralExpressionDataImpl -> NamedExpressionData; _GeneralExpressionData -> ExpressionData --- pyomo/core/base/expression.py | 119 ++++++++++++---------------------- 1 file changed, 41 insertions(+), 78 deletions(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index f5376381b2d..31bb5f835df 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -36,7 +36,7 @@ logger = logging.getLogger('pyomo.core') -class ExpressionData(numeric_expr.NumericValue): +class NamedExpressionData(numeric_expr.NumericValue): """ An object that defines a named expression. @@ -44,15 +44,14 @@ class ExpressionData(numeric_expr.NumericValue): expr The expression owned by this data. """ - __slots__ = () + __slots__ = ('_args_',) EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC PRECEDENCE = 0 ASSOCIATIVITY = EXPR.OperatorAssociativity.NON_ASSOCIATIVE - # - # Interface - # + def __init__(self, expr=None): + self._args_ = (expr,) def __call__(self, exception=True): """Compute the value of this expression.""" @@ -62,6 +61,18 @@ def __call__(self, exception=True): return arg return arg(exception=exception) + def create_node_with_local_data(self, values): + """ + Construct a simple expression after constructing the + contained expression. + + This class provides a consistent interface for constructing a + node, which is used in tree visitor scripts. + """ + obj = self.__class__() + obj._args_ = values + return obj + def is_named_expression_type(self): """A boolean indicating whether this in a named expression.""" return True @@ -110,9 +121,10 @@ def _compute_polynomial_degree(self, result): def _is_fixed(self, values): return values[0] - # - # Abstract Interface - # + # NamedExpressionData should never return False because + # they can store subexpressions that contain variables + def is_potentially_variable(self): + return True @property def expr(self): @@ -125,63 +137,6 @@ def expr(self): def expr(self, value): self.set_value(value) - def set_value(self, expr): - """Set the expression on this expression.""" - raise NotImplementedError - - def is_constant(self): - """A boolean indicating whether this expression is constant.""" - raise NotImplementedError - - def is_fixed(self): - """A boolean indicating whether this expression is fixed.""" - raise NotImplementedError - - # ExpressionData should never return False because - # they can store subexpressions that contain variables - def is_potentially_variable(self): - return True - - -class _ExpressionData(metaclass=RenamedClass): - __renamed__new_class__ = ExpressionData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralExpressionDataImpl(ExpressionData): - """ - An object that defines an expression that is never cloned - - Constructor Arguments - expr The Pyomo expression stored in this expression. - component The Expression object that owns this data. - - Public Class Attributes - expr The expression owned by this data. - """ - - __slots__ = () - - def __init__(self, expr=None): - self._args_ = (expr,) - - def create_node_with_local_data(self, values): - """ - Construct a simple expression after constructing the - contained expression. - - This class provides a consistent interface for constructing a - node, which is used in tree visitor scripts. - """ - obj = ScalarExpression() - obj.construct() - obj._args_ = values - return obj - - # - # Abstract Interface - # - def set_value(self, expr): """Set the expression on this expression.""" if expr is None or expr.__class__ in native_numeric_types: @@ -240,7 +195,16 @@ def __ipow__(self, other): return numeric_expr._pow_dispatcher[e.__class__, other.__class__](e, other) -class GeneralExpressionData(GeneralExpressionDataImpl, ComponentData): +class _ExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = NamedExpressionData + __renamed__version__ = '6.7.2.dev0' + +class _GeneralExpressionDataImpl(metaclass=RenamedClass): + __renamed__new_class__ = NamedExpressionData + __renamed__version__ = '6.7.2.dev0' + + +class ExpressionData(NamedExpressionData, ComponentData): """ An object that defines an expression that is never cloned @@ -255,17 +219,16 @@ class GeneralExpressionData(GeneralExpressionDataImpl, ComponentData): _component The expression component. """ - __slots__ = ('_args_',) + __slots__ = () def __init__(self, expr=None, component=None): - GeneralExpressionDataImpl.__init__(self, expr) - # Inlining ComponentData.__init__ + self._args_ = (expr,) self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET class _GeneralExpressionData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralExpressionData + __renamed__new_class__ = ExpressionData __renamed__version__ = '6.7.2.dev0' @@ -285,7 +248,7 @@ class Expression(IndexedComponent): doc Text describing this component. """ - _ComponentDataClass = GeneralExpressionData + _ComponentDataClass = ExpressionData # This seems like a copy-paste error, and should be renamed/removed NoConstraint = IndexedComponent.Skip @@ -412,9 +375,9 @@ def construct(self, data=None): timer.report() -class ScalarExpression(GeneralExpressionData, Expression): +class ScalarExpression(ExpressionData, Expression): def __init__(self, *args, **kwds): - GeneralExpressionData.__init__(self, expr=None, component=self) + ExpressionData.__init__(self, expr=None, component=self) Expression.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -437,7 +400,7 @@ def __call__(self, exception=True): def expr(self): """Return expression on this expression.""" if self._constructed: - return GeneralExpressionData.expr.fget(self) + return ExpressionData.expr.fget(self) raise ValueError( "Accessing the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -455,7 +418,7 @@ def clear(self): def set_value(self, expr): """Set the expression on this expression.""" if self._constructed: - return GeneralExpressionData.set_value(self, expr) + return ExpressionData.set_value(self, expr) raise ValueError( "Setting the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -465,7 +428,7 @@ def set_value(self, expr): def is_constant(self): """A boolean indicating whether this expression is constant.""" if self._constructed: - return GeneralExpressionData.is_constant(self) + return ExpressionData.is_constant(self) raise ValueError( "Accessing the is_constant flag of Expression '%s' " "before the Expression has been constructed (there " @@ -475,7 +438,7 @@ def is_constant(self): def is_fixed(self): """A boolean indicating whether this expression is fixed.""" if self._constructed: - return GeneralExpressionData.is_fixed(self) + return ExpressionData.is_fixed(self) raise ValueError( "Accessing the is_fixed flag of Expression '%s' " "before the Expression has been constructed (there " @@ -519,6 +482,6 @@ def add(self, index, expr): """Add an expression with a given index.""" if (type(expr) is tuple) and (expr == Expression.Skip): return None - cdata = GeneralExpressionData(expr, component=self) + cdata = ExpressionData(expr, component=self) self._data[index] = cdata return cdata From 74b79181869619d68778eb9361d431dad362361c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 22:40:12 -0600 Subject: [PATCH 0769/1178] Update *ExpressionData* referencces --- pyomo/contrib/appsi/cmodel/src/expression.hpp | 6 ++--- pyomo/contrib/cp/repn/docplex_writer.py | 6 ++--- .../logical_to_disjunctive_walker.py | 4 ++-- pyomo/contrib/fbbt/fbbt.py | 24 ++++++++----------- pyomo/contrib/latex_printer/latex_printer.py | 4 ++-- pyomo/contrib/mcpp/pyomo_mcpp.py | 6 +++-- pyomo/core/base/__init__.py | 6 +++-- pyomo/core/base/component.py | 2 +- pyomo/core/base/expression.py | 9 ++++--- pyomo/core/base/objective.py | 9 +++---- pyomo/core/expr/numeric_expr.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 4 ++-- pyomo/core/tests/unit/test_expression.py | 8 +++---- pyomo/core/tests/unit/test_list_objects.py | 4 ++-- pyomo/dae/integral.py | 4 ++-- pyomo/gdp/tests/test_util.py | 6 ++--- pyomo/repn/plugins/ampl/ampl_.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/standard_repn.py | 16 ++++++++----- pyomo/repn/util.py | 4 ++-- 20 files changed, 67 insertions(+), 63 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 803bb21b6e2..ad1234b3863 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -700,7 +700,7 @@ class PyomoExprTypes { expr_type_map[UnaryFunctionExpression] = unary_func; expr_type_map[NPV_UnaryFunctionExpression] = unary_func; expr_type_map[LinearExpression] = linear; - expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[_ExpressionData] = named_expr; expr_type_map[ScalarExpression] = named_expr; expr_type_map[Integral] = named_expr; expr_type_map[ScalarIntegral] = named_expr; @@ -765,8 +765,8 @@ class PyomoExprTypes { py::object NumericConstant = py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); py::object expr_module = py::module_::import("pyomo.core.base.expression"); - py::object _GeneralExpressionData = - expr_module.attr("_GeneralExpressionData"); + py::object _ExpressionData = + expr_module.attr("_ExpressionData"); py::object ScalarExpression = expr_module.attr("ScalarExpression"); py::object ScalarIntegral = py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 1af153910c0..37429d420d2 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -63,7 +63,7 @@ BooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, VarData, IndexedVar import pyomo.core.expr as EXPR @@ -949,7 +949,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): BeforeExpression: _handle_before_expression_node, AtExpression: _handle_at_expression_node, AlwaysIn: _handle_always_in_node, - GeneralExpressionData: _handle_named_expression_node, + ExpressionData: _handle_named_expression_node, ScalarExpression: _handle_named_expression_node, } _var_handles = { @@ -966,7 +966,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarBooleanVar: _before_boolean_var, BooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, - GeneralExpressionData: _before_named_expression, + ExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 0c493b89321..fdcfd5a8308 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -27,7 +27,7 @@ value, ) import pyomo.core.base.boolean_var as BV -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, VarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -217,7 +217,7 @@ def _dispatch_atmost(visitor, node, *args): # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var _before_child_dispatcher[VarData] = _dispatch_var -_before_child_dispatcher[GeneralExpressionData] = _dispatch_expression +_before_child_dispatcher[ExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 86f94506841..eb7155313c4 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import GeneralExpressionData, ScalarExpression +from pyomo.core.base.expression import ExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( @@ -333,15 +333,15 @@ def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): _unary_leaf_to_root_map[node.getname()](visitor, node, arg) -def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): +def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): """ Propagate bounds from children to parent Parameters ---------- visitor: _FBBTVisitorLeafToRoot - node: pyomo.core.base.expression.GeneralExpressionData - expr: GeneralExpression arg + node: pyomo.core.base.expression.ExpressionData + expr: NamedExpressionData arg """ bnds_dict = visitor.bnds_dict if node in bnds_dict: @@ -366,8 +366,8 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): numeric_expr.UnaryFunctionExpression: _prop_bnds_leaf_to_root_UnaryFunctionExpression, numeric_expr.LinearExpression: _prop_bnds_leaf_to_root_SumExpression, numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, - GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, - ScalarExpression: _prop_bnds_leaf_to_root_GeneralExpression, + ExpressionData: _prop_bnds_leaf_to_root_NamedExpression, + ScalarExpression: _prop_bnds_leaf_to_root_NamedExpression, }, ) @@ -898,13 +898,13 @@ def _prop_bnds_root_to_leaf_UnaryFunctionExpression(node, bnds_dict, feasibility ) -def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): """ Propagate bounds from parent to children. Parameters ---------- - node: pyomo.core.base.expression.GeneralExpressionData + node: pyomo.core.base.expression.ExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -945,12 +945,8 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): ) _prop_bnds_root_to_leaf_map[numeric_expr.AbsExpression] = _prop_bnds_root_to_leaf_abs -_prop_bnds_root_to_leaf_map[GeneralExpressionData] = ( - _prop_bnds_root_to_leaf_GeneralExpression -) -_prop_bnds_root_to_leaf_map[ScalarExpression] = ( - _prop_bnds_root_to_leaf_GeneralExpression -) +_prop_bnds_root_to_leaf_map[ExpressionData] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ScalarExpression] = _prop_bnds_root_to_leaf_NamedExpression def _check_and_reset_bounds(var, lb, ub): diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 13f30f899e4..cf286472a66 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -34,7 +34,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.objective import ScalarObjective, ObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( @@ -399,7 +399,7 @@ def __init__(self): EqualityExpression: handle_equality_node, InequalityExpression: handle_inequality_node, RangedExpression: handle_ranged_inequality_node, - GeneralExpressionData: handle_named_expression_node, + ExpressionData: handle_named_expression_node, ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, diff --git a/pyomo/contrib/mcpp/pyomo_mcpp.py b/pyomo/contrib/mcpp/pyomo_mcpp.py index 1375ae61c50..0ef0237681b 100644 --- a/pyomo/contrib/mcpp/pyomo_mcpp.py +++ b/pyomo/contrib/mcpp/pyomo_mcpp.py @@ -20,7 +20,7 @@ from pyomo.common.fileutils import Library from pyomo.core import value, Expression from pyomo.core.base.block import SubclassOf -from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.expr.numvalue import nonpyomo_leaf_types from pyomo.core.expr.numeric_expr import ( AbsExpression, @@ -307,7 +307,9 @@ def exitNode(self, node, data): ans = self.mcpp.newConstant(node) elif not node.is_expression_type(): ans = self.register_num(node) - elif type(node) in SubclassOf(Expression) or isinstance(node, ExpressionData): + elif type(node) in SubclassOf(Expression) or isinstance( + node, NamedExpressionData + ): ans = data[0] else: raise RuntimeError("Unhandled expression type: %s" % (type(node))) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index bcc2a0e0e02..3d1347659db 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -97,7 +97,7 @@ Constraint, ConstraintData, ) -from pyomo.core.base.expression import Expression, ExpressionData +from pyomo.core.base.expression import Expression, NamedExpressionData, ExpressionData from pyomo.core.base.external import ExternalFunction from pyomo.core.base.logical_constraint import ( LogicalConstraint, @@ -170,10 +170,12 @@ relocated_module_attribute( f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2.dev0' ) +relocated_module_attribute( + f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2.dev0' +) for _cdata in ( 'ConstraintData', 'LogicalConstraintData', - 'ExpressionData', 'VarData', 'BooleanVarData', 'ObjectiveData', diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 380aab23cbe..50cf264c799 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -803,7 +803,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, - # GeneralExpressionData, LogicalConstraintData, + # ExpressionData, LogicalConstraintData, # LogicalConstraintData, ObjectiveData, # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 31bb5f835df..10720366e28 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -44,15 +44,13 @@ class NamedExpressionData(numeric_expr.NumericValue): expr The expression owned by this data. """ - __slots__ = ('_args_',) + # Note: derived classes are expected to declare teh _args_ slot + __slots__ = () EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC PRECEDENCE = 0 ASSOCIATIVITY = EXPR.OperatorAssociativity.NON_ASSOCIATIVE - def __init__(self, expr=None): - self._args_ = (expr,) - def __call__(self, exception=True): """Compute the value of this expression.""" (arg,) = self._args_ @@ -199,6 +197,7 @@ class _ExpressionData(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData __renamed__version__ = '6.7.2.dev0' + class _GeneralExpressionDataImpl(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData __renamed__version__ = '6.7.2.dev0' @@ -219,7 +218,7 @@ class ExpressionData(NamedExpressionData, ComponentData): _component The expression component. """ - __slots__ = () + __slots__ = ('_args_',) def __init__(self, expr=None, component=None): self._args_ = (expr,) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fea356229fb..71cf5ba78f8 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -28,7 +28,7 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import ExpressionData, GeneralExpressionDataImpl +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, @@ -81,7 +81,7 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -class ObjectiveData(GeneralExpressionDataImpl, ActiveComponentData): +class ObjectiveData(NamedExpressionData, ActiveComponentData): """ This class defines the data for a single objective. @@ -104,10 +104,11 @@ class ObjectiveData(GeneralExpressionDataImpl, ActiveComponentData): _active A boolean that indicates whether this data is active """ - __slots__ = ("_sense", "_args_") + __slots__ = ("_args_", "_sense") def __init__(self, expr=None, sense=minimize, component=None): - GeneralExpressionDataImpl.__init__(self, expr) + # Inlining NamedExpressionData.__init__ + self._args_ = (expr,) # Inlining ActiveComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 50abaeedbba..49bb2e0280f 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -722,7 +722,7 @@ def args(self): @deprecated( 'The implicit recasting of a "not potentially variable" ' 'expression node to a potentially variable one is no ' - 'longer supported (this violates that immutability ' + 'longer supported (this violates the immutability ' 'promise for Pyomo5 expression trees).', version='6.4.3', ) diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index fae1d21a87e..16b7e0bd2e0 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import ObjectiveData -from pyomo.core.base.expression import GeneralExpressionData +from pyomo.core.base.expression import ExpressionData class _TestComponentDictBase(object): @@ -360,7 +360,7 @@ def setUp(self): class TestExpressionDict(_TestComponentDictBase, unittest.TestCase): _ctype = ExpressionDict - _cdatatype = GeneralExpressionData + _cdatatype = ExpressionData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index bf3ce0c2179..eb16f7c6142 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -29,7 +29,7 @@ value, sum_product, ) -from pyomo.core.base.expression import GeneralExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -515,10 +515,10 @@ def test_implicit_definition(self): model.E = Expression(model.idx) self.assertEqual(len(model.E), 3) expr = model.E[1] - self.assertIs(type(expr), GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) model.E[1] = None self.assertIs(expr, model.E[1]) - self.assertIs(type(expr), GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) @@ -537,7 +537,7 @@ def test_explicit_skip_definition(self): model.E[1] = None expr = model.E[1] - self.assertIs(type(expr), GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 32f2fa328cf..94913bcbc02 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import ObjectiveData -from pyomo.core.base.expression import GeneralExpressionData +from pyomo.core.base.expression import ExpressionData class _TestComponentListBase(object): @@ -377,7 +377,7 @@ def setUp(self): class TestExpressionList(_TestComponentListBase, unittest.TestCase): _ctype = XExpressionList - _cdatatype = GeneralExpressionData + _cdatatype = ExpressionData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index f767e31f18c..8c9512d98dd 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -14,7 +14,7 @@ from pyomo.core.base.indexed_component import rule_wrapper from pyomo.core.base.expression import ( Expression, - GeneralExpressionData, + ExpressionData, ScalarExpression, IndexedExpression, ) @@ -151,7 +151,7 @@ class ScalarIntegral(ScalarExpression, Integral): """ def __init__(self, *args, **kwds): - GeneralExpressionData.__init__(self, None, component=self) + ExpressionData.__init__(self, None, component=self) Integral.__init__(self, *args, **kwds) def clear(self): diff --git a/pyomo/gdp/tests/test_util.py b/pyomo/gdp/tests/test_util.py index 8ea72af37da..fa8e953f9f7 100644 --- a/pyomo/gdp/tests/test_util.py +++ b/pyomo/gdp/tests/test_util.py @@ -13,7 +13,7 @@ from pyomo.core import ConcreteModel, Var, Expression, Block, RangeSet, Any import pyomo.core.expr as EXPR -from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.gdp.util import ( clone_without_expression_components, is_child_of, @@ -40,7 +40,7 @@ def test_clone_without_expression_components(self): test = clone_without_expression_components(base, {}) self.assertIsNot(base, test) self.assertEqual(base(), test()) - self.assertIsInstance(base, ExpressionData) + self.assertIsInstance(base, NamedExpressionData) self.assertIsInstance(test, EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1, test()) @@ -51,7 +51,7 @@ def test_clone_without_expression_components(self): self.assertEqual(base(), test()) self.assertIsInstance(base, EXPR.SumExpression) self.assertIsInstance(test, EXPR.SumExpression) - self.assertIsInstance(base.arg(0), ExpressionData) + self.assertIsInstance(base.arg(0), NamedExpressionData) self.assertIsInstance(test.arg(0), EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1 + 3, test()) diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index 1cff45b30c1..cc99e9cfdae 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -33,7 +33,7 @@ from pyomo.core.base import ( SymbolMap, NameLabeler, - ExpressionData, + NamedExpressionData, SortComponents, var, param, @@ -724,7 +724,7 @@ def _print_nonlinear_terms_NL(self, exp): self._print_nonlinear_terms_NL(exp.arg(0)) self._print_nonlinear_terms_NL(exp.arg(1)) - elif isinstance(exp, (ExpressionData, IIdentityExpression)): + elif isinstance(exp, (NamedExpressionData, IIdentityExpression)): self._print_nonlinear_terms_NL(exp.expr) else: diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3c3c8539294..8cc73b5e3fe 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -70,7 +70,7 @@ ) from pyomo.core.base.component import ActiveComponent from pyomo.core.base.constraint import ConstraintData -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.objective import ScalarObjective, ObjectiveData from pyomo.core.base.suffix import SuffixFinder from pyomo.core.base.var import VarData diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index a23ebf6bb4f..b767ab727af 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -20,8 +20,12 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import ObjectiveData, ScalarObjective -from pyomo.core.base import ExpressionData, Expression -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base import Expression +from pyomo.core.base.expression import ( + ScalarExpression, + NamedExpressionData, + ExpressionData, +) from pyomo.core.base.var import ScalarVar, Var, VarData, value from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone @@ -1148,11 +1152,11 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra Var: _collect_var, variable: _collect_var, IVariable: _collect_var, - GeneralExpressionData: _collect_identity, + ExpressionData: _collect_identity, ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, - ExpressionData: _collect_identity, + NamedExpressionData: _collect_identity, Expression: _collect_identity, ObjectiveData: _collect_identity, ScalarObjective: _collect_identity, @@ -1547,11 +1551,11 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): Var : _linear_collect_var, variable : _linear_collect_var, IVariable : _linear_collect_var, - GeneralExpressionData : _linear_collect_identity, + ExpressionData : _linear_collect_identity, ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, - ExpressionData : _linear_collect_identity, + NamedExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, ObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 7351ea51c58..9a8713c6965 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -40,7 +40,7 @@ SortComponents, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.expr.numvalue import is_fixed, value import pyomo.core.expr as EXPR import pyomo.core.kernel as kernel @@ -55,7 +55,7 @@ EXPR.NPV_SumExpression, } _named_subexpression_types = ( - ExpressionData, + NamedExpressionData, kernel.expression.expression, kernel.objective.objective, ) From 47bb04f00f591f175a7a65e656197a41fc0eae50 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 23:29:02 -0600 Subject: [PATCH 0770/1178] Add missing method from GeneralLogicalConstraintData merge --- pyomo/core/base/logical_constraint.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 1daa5f83e90..9584078307d 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -77,6 +77,12 @@ def __init__(self, expr=None, component=None): if expr is not None: self.set_value(expr) + def __call__(self, exception=True): + """Compute the value of the body of this logical constraint.""" + if self.body is None: + return None + return self.body(exception=exception) + # # Abstract Interface # From d26a83ff243c7412bcb978f0f571b53ebc737f6c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 10:47:13 -0600 Subject: [PATCH 0771/1178] NFC: fix typo --- pyomo/core/base/expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 10720366e28..5638e48ea8b 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -44,7 +44,7 @@ class NamedExpressionData(numeric_expr.NumericValue): expr The expression owned by this data. """ - # Note: derived classes are expected to declare teh _args_ slot + # Note: derived classes are expected to declare the _args_ slot __slots__ = () EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC From db7b6b22ad1c2f3659fce2cb0cd96c69727ef6f3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 10:47:52 -0600 Subject: [PATCH 0772/1178] Update ComponentData references in APPSI --- pyomo/contrib/appsi/cmodel/src/expression.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index ad1234b3863..e91ca0af3b3 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -680,10 +680,10 @@ class PyomoExprTypes { expr_type_map[np_float32] = py_float; expr_type_map[np_float64] = py_float; expr_type_map[ScalarVar] = var; - expr_type_map[_VarData] = var; + expr_type_map[VarData] = var; expr_type_map[AutoLinkedBinaryVar] = var; expr_type_map[ScalarParam] = param; - expr_type_map[_ParamData] = param; + expr_type_map[ParamData] = param; expr_type_map[MonomialTermExpression] = product; expr_type_map[ProductExpression] = product; expr_type_map[NPV_ProductExpression] = product; @@ -700,7 +700,7 @@ class PyomoExprTypes { expr_type_map[UnaryFunctionExpression] = unary_func; expr_type_map[NPV_UnaryFunctionExpression] = unary_func; expr_type_map[LinearExpression] = linear; - expr_type_map[_ExpressionData] = named_expr; + expr_type_map[ExpressionData] = named_expr; expr_type_map[ScalarExpression] = named_expr; expr_type_map[Integral] = named_expr; expr_type_map[ScalarIntegral] = named_expr; @@ -728,12 +728,12 @@ class PyomoExprTypes { py::type np_float64 = np.attr("float64"); py::object ScalarParam = py::module_::import("pyomo.core.base.param").attr("ScalarParam"); - py::object _ParamData = - py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ParamData = + py::module_::import("pyomo.core.base.param").attr("ParamData"); py::object ScalarVar = py::module_::import("pyomo.core.base.var").attr("ScalarVar"); - py::object _VarData = - py::module_::import("pyomo.core.base.var").attr("_VarData"); + py::object VarData = + py::module_::import("pyomo.core.base.var").attr("VarData"); py::object AutoLinkedBinaryVar = py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); @@ -765,8 +765,8 @@ class PyomoExprTypes { py::object NumericConstant = py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); py::object expr_module = py::module_::import("pyomo.core.base.expression"); - py::object _ExpressionData = - expr_module.attr("_ExpressionData"); + py::object ExpressionData = + expr_module.attr("ExpressionData"); py::object ScalarExpression = expr_module.attr("ScalarExpression"); py::object ScalarIntegral = py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); From 5b48eb28989c29f23324d6d3e6cdf1ca452346fc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 12:18:53 -0600 Subject: [PATCH 0773/1178] Merge GeneralConstraintData into ConstraintData --- pyomo/core/base/constraint.py | 244 ++++++++++------------------------ 1 file changed, 70 insertions(+), 174 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 3455d2dde3c..08c97d7c8ae 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -125,17 +125,13 @@ def C_rule(model, i, j): return rule_wrapper(rule, result_map, map_types=map_types) -# -# This class is a pure interface -# - - -class ConstraintData(ActiveComponentData): +class ConstraintData(ConstraintData): """ - This class defines the data for a single constraint. + This class defines the data for a single general constraint. Constructor arguments: component The Constraint object that owns this data. + expr The Pyomo expression stored in this constraint. Public class attributes: active A boolean that is true if this constraint is @@ -155,164 +151,12 @@ class ConstraintData(ActiveComponentData): _active A boolean that indicates whether this data is active """ - __slots__ = () + __slots__ = ('_body', '_lower', '_upper', '_expr') # Set to true when a constraint class stores its expression # in linear canonical form _linear_canonical_form = False - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - ConstraintData, - # - ActiveComponentData - # - ComponentData - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._active = True - - # - # Interface - # - - def __call__(self, exception=True): - """Compute the value of the body of this constraint.""" - return value(self.body, exception=exception) - - def has_lb(self): - """Returns :const:`False` when the lower bound is - :const:`None` or negative infinity""" - return self.lb is not None - - def has_ub(self): - """Returns :const:`False` when the upper bound is - :const:`None` or positive infinity""" - return self.ub is not None - - def lslack(self): - """ - Returns the value of f(x)-L for constraints of the form: - L <= f(x) (<= U) - (U >=) f(x) >= L - """ - lb = self.lb - if lb is None: - return _inf - else: - return value(self.body) - lb - - def uslack(self): - """ - Returns the value of U-f(x) for constraints of the form: - (L <=) f(x) <= U - U >= f(x) (>= L) - """ - ub = self.ub - if ub is None: - return _inf - else: - return ub - value(self.body) - - def slack(self): - """ - Returns the smaller of lslack and uslack values - """ - lb = self.lb - ub = self.ub - body = value(self.body) - if lb is None: - return ub - body - elif ub is None: - return body - lb - return min(ub - body, body - lb) - - # - # Abstract Interface - # - - @property - def body(self): - """Access the body of a constraint expression.""" - raise NotImplementedError - - @property - def lower(self): - """Access the lower bound of a constraint expression.""" - raise NotImplementedError - - @property - def upper(self): - """Access the upper bound of a constraint expression.""" - raise NotImplementedError - - @property - def lb(self): - """Access the value of the lower bound of a constraint expression.""" - raise NotImplementedError - - @property - def ub(self): - """Access the value of the upper bound of a constraint expression.""" - raise NotImplementedError - - @property - def equality(self): - """A boolean indicating whether this is an equality constraint.""" - raise NotImplementedError - - @property - def strict_lower(self): - """True if this constraint has a strict lower bound.""" - raise NotImplementedError - - @property - def strict_upper(self): - """True if this constraint has a strict upper bound.""" - raise NotImplementedError - - def set_value(self, expr): - """Set the expression on this constraint.""" - raise NotImplementedError - - def get_value(self): - """Get the expression on this constraint.""" - raise NotImplementedError - - -class _ConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = ConstraintData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralConstraintData(ConstraintData): - """ - This class defines the data for a single general constraint. - - Constructor arguments: - component The Constraint object that owns this data. - expr The Pyomo expression stored in this constraint. - - Public class attributes: - active A boolean that is true if this constraint is - active in the model. - body The Pyomo expression for this constraint - lower The Pyomo expression for the lower bound - upper The Pyomo expression for the upper bound - equality A boolean that indicates whether this is an - equality constraint - strict_lower A boolean that indicates whether this - constraint uses a strict lower bound - strict_upper A boolean that indicates whether this - constraint uses a strict upper bound - - Private class attributes: - _component The objective component. - _active A boolean that indicates whether this data is active - """ - - __slots__ = ('_body', '_lower', '_upper', '_expr') - def __init__(self, expr=None, component=None): # # These lines represent in-lining of the @@ -330,9 +174,9 @@ def __init__(self, expr=None, component=None): if expr is not None: self.set_value(expr) - # - # Abstract Interface - # + def __call__(self, exception=True): + """Compute the value of the body of this constraint.""" + return value(self.body, exception=exception) @property def body(self): @@ -456,6 +300,16 @@ def strict_upper(self): """True if this constraint has a strict upper bound.""" return False + def has_lb(self): + """Returns :const:`False` when the lower bound is + :const:`None` or negative infinity""" + return self.lb is not None + + def has_ub(self): + """Returns :const:`False` when the upper bound is + :const:`None` or positive infinity""" + return self.ub is not None + @property def expr(self): """Return the expression associated with this constraint.""" @@ -683,9 +537,51 @@ def set_value(self, expr): "upper bound (%s)." % (self.name, self._upper) ) + def lslack(self): + """ + Returns the value of f(x)-L for constraints of the form: + L <= f(x) (<= U) + (U >=) f(x) >= L + """ + lb = self.lb + if lb is None: + return _inf + else: + return value(self.body) - lb + + def uslack(self): + """ + Returns the value of U-f(x) for constraints of the form: + (L <=) f(x) <= U + U >= f(x) (>= L) + """ + ub = self.ub + if ub is None: + return _inf + else: + return ub - value(self.body) + + def slack(self): + """ + Returns the smaller of lslack and uslack values + """ + lb = self.lb + ub = self.ub + body = value(self.body) + if lb is None: + return ub - body + elif ub is None: + return body - lb + return min(ub - body, body - lb) + + +class _ConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = ConstraintData + __renamed__version__ = '6.7.2.dev0' + class _GeneralConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralConstraintData + __renamed__new_class__ = ConstraintData __renamed__version__ = '6.7.2.dev0' @@ -731,7 +627,7 @@ class Constraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = GeneralConstraintData + _ComponentDataClass = ConstraintData class Infeasible(object): pass @@ -889,14 +785,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarConstraint(GeneralConstraintData, Constraint): +class ScalarConstraint(ConstraintData, Constraint): """ ScalarConstraint is the implementation representing a single, non-indexed constraint. """ def __init__(self, *args, **kwds): - GeneralConstraintData.__init__(self, component=self, expr=None) + ConstraintData.__init__(self, component=self, expr=None) Constraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -920,7 +816,7 @@ def body(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.body.fget(self) + return ConstraintData.body.fget(self) @property def lower(self): @@ -932,7 +828,7 @@ def lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.lower.fget(self) + return ConstraintData.lower.fget(self) @property def upper(self): @@ -944,7 +840,7 @@ def upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.upper.fget(self) + return ConstraintData.upper.fget(self) @property def equality(self): @@ -956,7 +852,7 @@ def equality(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.equality.fget(self) + return ConstraintData.equality.fget(self) @property def strict_lower(self): @@ -968,7 +864,7 @@ def strict_lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.strict_lower.fget(self) + return ConstraintData.strict_lower.fget(self) @property def strict_upper(self): @@ -980,7 +876,7 @@ def strict_upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.strict_upper.fget(self) + return ConstraintData.strict_upper.fget(self) def clear(self): self._data = {} @@ -1045,7 +941,7 @@ def add(self, index, expr): return self.__setitem__(index, expr) @overload - def __getitem__(self, index) -> GeneralConstraintData: ... + def __getitem__(self, index) -> ConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore From 97ad3c0945ca1426729a107320a06c21d54653c0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 15:15:03 -0600 Subject: [PATCH 0774/1178] Update references from GeneralConstraintData to ConstraintData --- pyomo/contrib/appsi/base.py | 48 +++++++++---------- pyomo/contrib/appsi/fbbt.py | 6 +-- pyomo/contrib/appsi/solvers/cbc.py | 6 +-- pyomo/contrib/appsi/solvers/cplex.py | 10 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 16 +++---- pyomo/contrib/appsi/solvers/highs.py | 6 +-- pyomo/contrib/appsi/solvers/ipopt.py | 8 ++-- pyomo/contrib/appsi/solvers/wntr.py | 6 +-- pyomo/contrib/appsi/writers/lp_writer.py | 6 +-- pyomo/contrib/appsi/writers/nl_writer.py | 6 +-- pyomo/contrib/solver/base.py | 10 ++-- pyomo/contrib/solver/gurobi.py | 16 +++---- pyomo/contrib/solver/persistent.py | 12 ++--- pyomo/contrib/solver/solution.py | 14 +++--- .../contrib/solver/tests/unit/test_results.py | 6 +-- pyomo/core/tests/unit/test_con.py | 4 +- pyomo/core/tests/unit/test_dict_objects.py | 4 +- pyomo/core/tests/unit/test_list_objects.py | 4 +- pyomo/gdp/tests/common_tests.py | 6 +-- pyomo/gdp/tests/test_bigm.py | 4 +- pyomo/gdp/tests/test_hull.py | 6 +-- .../plugins/solvers/gurobi_persistent.py | 10 ++-- 22 files changed, 105 insertions(+), 109 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 9d00a56e8b9..6655ec26524 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -21,7 +21,7 @@ Tuple, MutableMapping, ) -from pyomo.core.base.constraint import GeneralConstraintData, Constraint +from pyomo.core.base.constraint import ConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData, Var from pyomo.core.base.param import ParamData, Param @@ -214,8 +214,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -233,8 +233,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to slack. @@ -317,8 +317,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' @@ -334,8 +334,8 @@ def get_duals( return duals def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._slacks is None: raise RuntimeError( 'Solution loader does not currently have valid slacks. Please ' @@ -727,8 +727,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Declare sign convention in docstring here. @@ -748,8 +748,8 @@ def get_duals( ) def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Parameters ---------- @@ -803,7 +803,7 @@ def add_params(self, params: List[ParamData]): pass @abc.abstractmethod - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): pass @abc.abstractmethod @@ -819,7 +819,7 @@ def remove_params(self, params: List[ParamData]): pass @abc.abstractmethod - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): pass @abc.abstractmethod @@ -853,14 +853,14 @@ def get_primals(self, vars_to_load=None): return self._solver.get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_duals(cons_to_load=cons_to_load) def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_slacks(cons_to_load=cons_to_load) @@ -980,7 +980,7 @@ def add_params(self, params: List[ParamData]): self._add_params(params) @abc.abstractmethod - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): pass def _check_for_new_vars(self, variables: List[VarData]): @@ -1000,7 +1000,7 @@ def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): all_fixed_vars = dict() for con in cons: if con in self._named_expressions: @@ -1128,10 +1128,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): pass - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -1330,7 +1330,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, GeneralConstraintData) + c.ctype is None and isinstance(c, ConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 4b0d6d4876c..8e0c74b00e9 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -20,7 +20,7 @@ from typing import List, Optional from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.objective import ObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData @@ -154,7 +154,7 @@ def _add_params(self, params: List[ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_fbbt_constraints( self._cmodel, self._pyomo_expr_types, @@ -175,7 +175,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): if self._symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index dffd479a5c7..08833e747e2 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -27,7 +27,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData from pyomo.core.base.objective import ObjectiveData @@ -170,7 +170,7 @@ def add_variables(self, variables: List[VarData]): def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -182,7 +182,7 @@ def remove_variables(self, variables: List[VarData]): def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 22c11bdfbe8..10de981ce7d 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -23,7 +23,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData from pyomo.core.base.objective import ObjectiveData @@ -185,7 +185,7 @@ def add_variables(self, variables: List[VarData]): def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -197,7 +197,7 @@ def remove_variables(self, variables: List[VarData]): def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -389,8 +389,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index e2ecd9b69e7..2719ecc2a00 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -24,7 +24,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import Var, VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -579,7 +579,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -735,7 +735,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1195,7 +1195,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1272,7 +1272,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1304,7 +1304,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1425,7 +1425,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -1510,7 +1510,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index c3083ac78d3..6410700c569 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -21,7 +21,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant @@ -376,7 +376,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -462,7 +462,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 4144fbbecd9..76cd204e36d 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -29,7 +29,7 @@ from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData from pyomo.core.base.objective import ObjectiveData @@ -234,7 +234,7 @@ def add_variables(self, variables: List[VarData]): def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -246,7 +246,7 @@ def remove_variables(self, variables: List[VarData]): def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -534,7 +534,7 @@ def get_primals( res[v] = self._primal_sol[v] return res - def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None): + def get_duals(self, cons_to_load: Optional[Sequence[ConstraintData]] = None): if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 62c4b0ed358..0a66cc640e5 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -42,7 +42,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import @@ -278,7 +278,7 @@ def _add_params(self, params: List[ParamData]): setattr(self._solver_model, pname, wntr_p) self._pyomo_param_to_solver_param_map[id(p)] = wntr_p - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): aml = wntr.sim.aml.aml for con in cons: if not con.equality: @@ -294,7 +294,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): self._pyomo_con_to_solver_con_map[con] = wntr_con self._needs_updated = True - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: solver_con = self._pyomo_con_to_solver_con_map[con] delattr(self._solver_model, solver_con.name) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 3a6193bd314..788dfde7892 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData @@ -99,14 +99,14 @@ def _add_params(self, params: List[ParamData]): cp.value = p.value self._pyomo_param_to_solver_param_map[id(p)] = cp - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_lp_constraints(cons, self) def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for c in cons: cc = self._pyomo_con_to_solver_con_map.pop(c) self._writer.remove_constraint(cc) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 754bd179497..27cdca004cb 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData @@ -111,7 +111,7 @@ def _add_params(self, params: List[ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_nl_constraints( self._writer, self._expr_types, @@ -130,7 +130,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): if self.config.symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index c53f917bc2a..45b5cca0179 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,7 +14,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData @@ -230,8 +230,8 @@ def _get_primals( ) def _get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Declare sign convention in docstring here. @@ -292,7 +292,7 @@ def add_parameters(self, params: List[ParamData]): """ @abc.abstractmethod - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): """ Add constraints to the model """ @@ -316,7 +316,7 @@ def remove_parameters(self, params: List[ParamData]): """ @abc.abstractmethod - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): """ Remove constraints from the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index ff4e93f7635..10d8120c8b3 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -23,7 +23,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -555,7 +555,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -711,7 +711,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1125,7 +1125,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1202,7 +1202,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1234,7 +1234,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1355,7 +1355,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -1440,7 +1440,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 81d0df1334f..71322b7043e 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -12,7 +12,7 @@ import abc from typing import List -from pyomo.core.base.constraint import GeneralConstraintData, Constraint +from pyomo.core.base.constraint import ConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData, Param @@ -84,7 +84,7 @@ def add_parameters(self, params: List[ParamData]): self._add_parameters(params) @abc.abstractmethod - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): pass def _check_for_new_vars(self, variables: List[VarData]): @@ -104,7 +104,7 @@ def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): all_fixed_vars = {} for con in cons: if con in self._named_expressions: @@ -209,10 +209,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): pass - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -384,7 +384,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, GeneralConstraintData) + c.ctype is None and isinstance(c, ConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index e089e621f1f..a3e66475982 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -12,7 +12,7 @@ import abc from typing import Sequence, Dict, Optional, Mapping, NoReturn -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.var import VarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap @@ -65,8 +65,8 @@ def get_primals( """ def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -119,8 +119,8 @@ def get_primals(self, vars_to_load=None): return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver._get_duals(cons_to_load=cons_to_load) @@ -201,8 +201,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 6c178d80298..a15c9b87253 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -15,7 +15,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.var import VarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results @@ -67,8 +67,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index 26ccc7944a7..15f190e281e 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -44,7 +44,7 @@ InequalityExpression, RangedExpression, ) -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData class TestConstraintCreation(unittest.TestCase): @@ -1074,7 +1074,7 @@ def test_setitem(self): m.c[2] = m.x**2 <= 4 self.assertEqual(len(m.c), 1) self.assertEqual(list(m.c.keys()), [2]) - self.assertIsInstance(m.c[2], GeneralConstraintData) + self.assertIsInstance(m.c[2], ConstraintData) self.assertEqual(m.c[2].upper, 4) m.c[3] = Constraint.Skip diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 16b7e0bd2e0..ef9f330bfff 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -18,7 +18,7 @@ ExpressionDict, ) from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import ExpressionData @@ -375,7 +375,7 @@ def setUp(self): class TestConstraintDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ConstraintDict - _cdatatype = GeneralConstraintData + _cdatatype = ConstraintData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 94913bcbc02..671a8429e06 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -18,7 +18,7 @@ XExpressionList, ) from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import ExpressionData @@ -392,7 +392,7 @@ def setUp(self): class TestConstraintList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XConstraintList - _cdatatype = GeneralConstraintData + _cdatatype = ConstraintData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 233c3ca9c09..50bc8b05f86 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -952,9 +952,7 @@ def check_disjunction_data_target(self, transformation): transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation) self.assertIsInstance(transBlock, Block) self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - self.assertIsInstance( - transBlock.disjunction_xor[2], constraint.GeneralConstraintData - ) + self.assertIsInstance(transBlock.disjunction_xor[2], constraint.ConstraintData) self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block) self.assertEqual(len(transBlock.relaxedDisjuncts), 3) @@ -963,7 +961,7 @@ def check_disjunction_data_target(self, transformation): m, targets=[m.disjunction[1]] ) self.assertIsInstance( - m.disjunction[1].algebraic_constraint, constraint.GeneralConstraintData + m.disjunction[1].algebraic_constraint, constraint.ConstraintData ) transBlock = m.component("_pyomo_gdp_%s_reformulation_4" % transformation) self.assertIsInstance(transBlock, Block) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index efef4c5fb1f..cf42eb260ff 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1323,8 +1323,8 @@ def test_do_not_transform_deactivated_constraintDatas(self): self.assertEqual(len(cons_list), 2) lb = cons_list[0] ub = cons_list[1] - self.assertIsInstance(lb, constraint.GeneralConstraintData) - self.assertIsInstance(ub, constraint.GeneralConstraintData) + self.assertIsInstance(lb, constraint.ConstraintData) + self.assertIsInstance(ub, constraint.ConstraintData) def checkMs( self, m, disj1c1lb, disj1c1ub, disj1c2lb, disj1c2ub, disj2c1ub, disj2c2ub diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 6093e01dc25..07876a9d213 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1252,12 +1252,10 @@ def check_second_iteration(self, model): orig = model.component("_pyomo_gdp_hull_reformulation") self.assertIsInstance( - model.disjunctionList[1].algebraic_constraint, - constraint.GeneralConstraintData, + model.disjunctionList[1].algebraic_constraint, constraint.ConstraintData ) self.assertIsInstance( - model.disjunctionList[0].algebraic_constraint, - constraint.GeneralConstraintData, + model.disjunctionList[0].algebraic_constraint, constraint.ConstraintData ) self.assertFalse(model.disjunctionList[1].active) self.assertFalse(model.disjunctionList[0].active) diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 17ce33fd95f..94a2ac6b734 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -157,7 +157,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -384,7 +384,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -431,7 +431,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -569,7 +569,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -647,7 +647,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: From ef522d9338eb8722737d0522fe65bfd8e3c96ec9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 15:25:22 -0600 Subject: [PATCH 0775/1178] Fix base class --- pyomo/core/base/constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 08c97d7c8ae..3a71758d55d 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -125,7 +125,7 @@ def C_rule(model, i, j): return rule_wrapper(rule, result_map, map_types=map_types) -class ConstraintData(ConstraintData): +class ConstraintData(ActiveComponentData): """ This class defines the data for a single general constraint. From e55007991d8ec0cadc84bc12bbddfcb01225bdcb Mon Sep 17 00:00:00 2001 From: Eslick Date: Fri, 22 Mar 2024 14:47:31 -0400 Subject: [PATCH 0776/1178] Fix for duplicate add --- pyomo/solvers/plugins/solvers/ASL.py | 4 +++- pyomo/solvers/plugins/solvers/IPOPT.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index ae7ad82c870..3ebe5c3b422 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -160,7 +160,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC'] + for line in env['PYOMO_AMPLFUNC'].split('\n'): + if line not in env['AMPLFUNC']: + env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 4ebbbc07d3b..17b68da6364 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -121,7 +121,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC'] + for line in env['PYOMO_AMPLFUNC'].split('\n'): + if line not in env['AMPLFUNC']: + env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] From 035cf9c6ff5e3482a283fb6ea21714db35a56709 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Mar 2024 18:57:03 -0400 Subject: [PATCH 0777/1178] replace an error check that should never happen with a comment --- pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py index edb1a03afe6..d5de010f308 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -191,9 +191,8 @@ def x_constraint(b, i): # Not a Gray code, just a regular binary representation # TODO test the Gray codes too + # note: Must have num != 0 and ceil(log2(num)) > length to be valid def _get_binary_vector(self, num, length): - if num != 0 and ceil(log2(num)) > length: - raise DeveloperError("Invalid input in _get_binary_vector") # Use python's string formatting instead of bothering with modular # arithmetic. Hopefully not slow. return tuple(int(x) for x in format(num, f"0{length}b")) From 9f63effd966bbe232e4774d26c27cfb242391976 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 22 Mar 2024 21:16:49 -0600 Subject: [PATCH 0778/1178] initial implementation of function to perform the full (fine and coarse) dulmage-mendelsohn decomposition --- pyomo/contrib/incidence_analysis/config.py | 7 +++ pyomo/contrib/incidence_analysis/interface.py | 47 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 128273b4dec..2a7734ba433 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -36,6 +36,13 @@ class IncidenceMethod(enum.Enum): """Use ``pyomo.repn.plugins.nl_writer.AMPLRepnVisitor``""" +class IncidenceOrder(enum.Enum): + + dulmage_mendelsohn_upper = 0 + + dulmage_mendelsohn_lower = 1 + + _include_fixed = ConfigValue( default=False, domain=bool, diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 50cb84daaf5..2136a4ffc24 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -29,7 +29,7 @@ plotly, ) from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import get_config_from_kwds +from pyomo.contrib.incidence_analysis.config import get_config_from_kwds, IncidenceOrder from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -995,3 +995,48 @@ def add_edge(self, variable, constraint): con_id = self._con_index_map[constraint] self._incidence_graph.add_edge(var_id, con_id) + + def partition_variables_and_constraints( + self, + variables=None, + constraints=None, + order=IncidenceOrder.dulmage_mendelsohn_upper, + ): + """Partition variables and constraints in an incidence graph + """ + variables, constraints = self._validate_input(variables, constraints) + vdmp, cdmp = self.dulmage_mendelsohn(variables=variables, constraints=constraints) + + ucv = vdmp.unmatched + vdmp.underconstrained + ucc = cdmp.underconstrained + + ocv = vdmp.overconstrained + occ = cdmp.overconstrained + cdmp.unmatched + + ucvblocks, uccblocks = self.get_connected_components( + variables=ucv, constraints=ucc + ) + ocvblocks, occblocks = self.get_connected_components( + variables=ocv, constraints=occ + ) + wcvblocks, wccblocks = self.block_triangularize( + variables=vdmp.square, constraints=cdmp.square + ) + # By default, we block-*lower* triangularize. By default, however, we want + # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. + wcvblocks.reverse() + wccblocks.reverse() + vpartition = [ucvblocks, wcvblocks, ocvblocks] + cpartition = [uccblocks, wccblocks, occblocks] + + if order == IncidenceOrder.dulmage_mendelsohn_lower: + # If a block-lower triangular matrix was requested, we need to reverse + # both the inner and outer partitions + vpartition.reverse() + cpartition.reverse() + for vb in vpartition: + vb.reverse() + for cb in cpartition: + cb.reverse() + + return vpartition, cpartition From 87517fae3ec0a41f6abd93c39c8af5fe93494f4d Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 22 Mar 2024 22:41:52 -0600 Subject: [PATCH 0779/1178] draft of function to plot incidence matrix in dulmage-mendelsohn order --- pyomo/contrib/incidence_analysis/interface.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 2136a4ffc24..6e6dff7ba48 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -1040,3 +1040,84 @@ def partition_variables_and_constraints( cb.reverse() return vpartition, cpartition + + +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +def _get_rectangle_around_coords( + ij1, + ij2, + linewidth=2, +): + i1, j1 = ij1 + i2, j2 = ij2 + buffer = 0.5 + ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) + width = abs(i1 - i2) + 2*buffer + height = abs(j1 - j2) + 2*buffer + rect = Rectangle( + ll_corner, + width, + height, + clip_on=False, + fill=False, + edgecolor="orange", + linewidth=linewidth, + ) + return rect + + +def spy_dulmage_mendelsohn( + model, + order=IncidenceOrder.dulmage_mendelsohn_upper, + highlight_coarse=True, + highlight_fine=True, + ax=None, +): + igraph = IncidenceGraphInterface(model) + vpart, cpart = igraph.partition_variables_and_constraints(order=order) + vpart_fine = sum(vpart, []) + cpart_fine = sum(cpart, []) + vorder = sum(vpart_fine, []) + corder = sum(cpart_fine, []) + + imat = get_structural_incidence_matrix(vorder, corder) + + if ax is None: + fig, ax = plt.subplots() + else: + fig = None + + # TODO: Options to configure: + # - tick direction/presence + # - rectangle linewidth/linestyle + # - spy markersize + # markersize and linewidth should probably be set automatically + # based on size of problem + + ax.spy( + imat, + # TODO: pass keyword args + markersize=0.2, + ) + ax.tick_params(length=0) + if highlight_coarse: + start = (0, 0) + for vblocks, cblocks in zip(vpart, cpart): + # Get the total number of variables/constraints in this part + # of the coarse partition + nv = sum(len(vb) for vb in vblocks) + nc = sum(len(cb) for cb in cblocks) + stop = (start[0] + nv - 1, start[1] + nc - 1) + ax.add_patch(_get_rectangle_around_coords(start, stop)) + start = (stop[0] + 1, stop[1] + 1) + + if highlight_fine: + start = (0, 0) + for vb, cb in zip(vpart_fine, cpart_fine): + stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + ax.add_patch(_get_rectangle_around_coords(start, stop)) + start = (stop[0] + 1, stop[1] + 1) + + return fig, ax From a573244f008af69eceb39c0812cd3d30b09df268 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Mar 2024 06:32:30 -0600 Subject: [PATCH 0780/1178] Rename _ComponentBase -> ComponentBase --- pyomo/core/base/component.py | 12 +++++++++--- pyomo/core/base/set.py | 4 ++-- pyomo/core/util.py | 6 +++--- pyomo/gdp/util.py | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 50cf264c799..7dea5b7dde5 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -20,6 +20,7 @@ from pyomo.common.autoslots import AutoSlots, fast_deepcopy from pyomo.common.collections import OrderedDict from pyomo.common.deprecation import ( + RenamedClass, deprecated, deprecation_warning, relocated_module_attribute, @@ -79,7 +80,7 @@ class CloneError(pyomo.common.errors.PyomoException): pass -class _ComponentBase(PyomoObject): +class ComponentBase(PyomoObject): """A base class for Component and ComponentData This class defines some fundamental methods and properties that are @@ -474,7 +475,12 @@ def _pprint_base_impl( ostream.write(_data) -class Component(_ComponentBase): +class _ComponentBase(metaclass=RenamedClass): + __renamed__new_class__ = ComponentBase + __renamed__version__ = '6.7.2.dev0' + + +class Component(ComponentBase): """ This is the base class for all Pyomo modeling components. @@ -779,7 +785,7 @@ def deactivate(self): self._active = False -class ComponentData(_ComponentBase): +class ComponentData(ComponentBase): """ This is the base class for the component data used in Pyomo modeling components. Subclasses of ComponentData are diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index d94cc86cf7c..fbf1ac60900 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -50,7 +50,7 @@ RangeDifferenceError, ) from pyomo.core.base.component import ( - _ComponentBase, + ComponentBase, Component, ComponentData, ModelComponentFactory, @@ -140,7 +140,7 @@ def process_setarg(arg): _anonymous.update(arg._anonymous_sets) return arg, _anonymous - elif isinstance(arg, _ComponentBase): + elif isinstance(arg, ComponentBase): if isinstance(arg, IndexedComponent) and arg.is_indexed(): raise TypeError( "Cannot apply a Set operator to an " diff --git a/pyomo/core/util.py b/pyomo/core/util.py index f337b487cef..4b6cc8f3320 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -18,7 +18,7 @@ from pyomo.core.expr.numeric_expr import mutable_expression, NPV_SumExpression from pyomo.core.base.var import Var from pyomo.core.base.expression import Expression -from pyomo.core.base.component import _ComponentBase +from pyomo.core.base.component import ComponentBase import logging logger = logging.getLogger(__name__) @@ -238,12 +238,12 @@ def sequence(*args): def target_list(x): - if isinstance(x, _ComponentBase): + if isinstance(x, ComponentBase): return [x] elif hasattr(x, '__iter__'): ans = [] for i in x: - if isinstance(i, _ComponentBase): + if isinstance(i, ComponentBase): ans.append(i) else: raise ValueError( diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 686253b0179..932a2ddf451 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -13,7 +13,6 @@ from pyomo.gdp.disjunct import DisjunctData, Disjunct import pyomo.core.expr as EXPR -from pyomo.core.base.component import _ComponentBase from pyomo.core import ( Block, Suffix, From b75a974378012d4319668961f99a91668aab6446 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Mar 2024 06:46:04 -0600 Subject: [PATCH 0781/1178] Remove _SetDataBase --- pyomo/core/base/reference.py | 6 +++--- pyomo/core/base/set.py | 26 ++++++++++---------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 2279db067a6..fd6ba192c70 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -18,7 +18,7 @@ Sequence, ) from pyomo.common.modeling import NOTSET -from pyomo.core.base.set import DeclareGlobalSet, Set, SetOf, OrderedSetOf, _SetDataBase +from pyomo.core.base.set import DeclareGlobalSet, Set, SetOf, OrderedSetOf, SetData from pyomo.core.base.component import Component, ComponentData from pyomo.core.base.global_set import UnindexedComponent_set from pyomo.core.base.enums import SortComponents @@ -774,10 +774,10 @@ def Reference(reference, ctype=NOTSET): # is that within the subsets list, and set is a wildcard set. index = wildcards[0][1] # index is the first wildcard set. - if not isinstance(index, _SetDataBase): + if not isinstance(index, SetData): index = SetOf(index) for lvl, idx in wildcards[1:]: - if not isinstance(idx, _SetDataBase): + if not isinstance(idx, SetData): idx = SetOf(idx) index = index * idx # index is now either a single Set, or a SetProduct of the diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index fbf1ac60900..b9a2fe72e1d 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -84,10 +84,7 @@ All Sets implement one of the following APIs: -0. `class _SetDataBase(ComponentData)` - *(pure virtual interface)* - -1. `class SetData(_SetDataBase)` +1. `class SetData(ComponentData)` *(base class for all AML Sets)* 2. `class _FiniteSetMixin(object)` @@ -128,7 +125,7 @@ def process_setarg(arg): - if isinstance(arg, _SetDataBase): + if isinstance(arg, SetData): if ( getattr(arg, '_parent', None) is not None or getattr(arg, '_anonymous_sets', None) is GlobalSetBase @@ -512,16 +509,8 @@ class _NotFound(object): pass -# A trivial class that we can use to test if an object is a "legitimate" -# set (either ScalarSet, or a member of an IndexedSet) -class _SetDataBase(ComponentData): - """The base for all objects that can be used as a component indexing set.""" - - __slots__ = () - - -class SetData(_SetDataBase): - """The base for all Pyomo AML objects that can be used as a component +class SetData(ComponentData): + """The base for all Pyomo objects that can be used as a component indexing set. Derived versions of this class can be used as the Index for any @@ -1193,6 +1182,11 @@ class _SetData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' +class _SetDataBase(metaclass=RenamedClass): + __renamed__new_class__ = SetData + __renamed__version__ = '6.7.2.dev0' + + class _FiniteSetMixin(object): __slots__ = () @@ -3496,7 +3490,7 @@ def _domain(self, val): def _checkArgs(*sets): ans = [] for s in sets: - if isinstance(s, _SetDataBase): + if isinstance(s, SetData): ans.append((s.isordered(), s.isfinite())) elif type(s) in {tuple, list}: ans.append((True, True)) From fb2212ad07ab7abbae86d4e1a0a7d98e894e07a1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 12:42:53 -0600 Subject: [PATCH 0782/1178] document plotting function and automatically calculate markersize if not provided --- pyomo/contrib/incidence_analysis/interface.py | 115 +++++++++++++++--- 1 file changed, 99 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 6e6dff7ba48..a4f08c737ec 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -1049,6 +1049,7 @@ def _get_rectangle_around_coords( ij1, ij2, linewidth=2, + linestyle="-", ): i1, j1 = ij1 i2, j2 = ij2 @@ -1064,18 +1065,82 @@ def _get_rectangle_around_coords( fill=False, edgecolor="orange", linewidth=linewidth, + linestyle=linestyle, ) return rect def spy_dulmage_mendelsohn( model, + *, + incidence_kwds=None, order=IncidenceOrder.dulmage_mendelsohn_upper, highlight_coarse=True, highlight_fine=True, + skip_wellconstrained=False, ax=None, + linewidth=2, + spy_kwds=None, ): - igraph = IncidenceGraphInterface(model) + """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib + axes + + This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting + an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine + partitions highlighted. The coarse partition refers to the under-constrained, + over-constrained, and well-constrained subsystems, while the fine partition + refers to block diagonal or block triangular partitions of the former + subsystems. + + Parameters + ---------- + + model: ``ConcreteModel`` + Input model to plot sparsity structure of + + incidence_kwds: dict, optional + Config options for ``IncidenceGraphInterface`` + + order: ``IncidenceOrder``, optional + Order in which to plot sparsity structure + + highlight_coarse: bool, optional + Whether to draw a rectange around the coarse partition + + highlight_fine: bool, optional + Whether to draw a rectangle around the fine partition + + skip_wellconstrained: bool, optional + Whether to skip highlighting the well-constrained subsystem of the + coarse partition. Default False + + ax: ``matplotlib.pyplot.Axes``, optional + Axes object on which to plot. If not provided, new figure + and axes are created. + + linewidth: int, optional + Line width of for rectangle used to highlight. Default 2 + + spy_kwds: dict, optional + Keyword arguments for ``Axes.spy`` + + Returns + ------- + + fig: ``matplotlib.pyplot.Figure`` or ``None`` + Figure on which the sparsity structure is plotted. ``None`` if axes + are provided + + ax: ``matplotlib.pyplot.Axes`` + Axes on which the sparsity structure is plotted + + """ + if incidence_kwds is None: + incidence_kwds = {} + if spy_kwds is None: + spy_kwds = {} + + igraph = IncidenceGraphInterface(model, **incidence_kwds) vpart, cpart = igraph.partition_variables_and_constraints(order=order) vpart_fine = sum(vpart, []) cpart_fine = sum(cpart, []) @@ -1083,41 +1148,59 @@ def spy_dulmage_mendelsohn( corder = sum(cpart_fine, []) imat = get_structural_incidence_matrix(vorder, corder) + nvar = len(vorder) + ncon = len(corder) if ax is None: fig, ax = plt.subplots() else: fig = None - # TODO: Options to configure: - # - tick direction/presence - # - rectangle linewidth/linestyle - # - spy markersize - # markersize and linewidth should probably be set automatically - # based on size of problem - - ax.spy( - imat, - # TODO: pass keyword args - markersize=0.2, - ) + markersize = spy_kwds.pop("markersize", None) + if markersize is None: + # At 10000 vars/cons, we want markersize=0.2 + # At 20 vars/cons, we want markersize=10 + # We assume we want a linear relationship between 1/nvar + # and the markersize. + markersize = ( + (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) + + 0.2 + ) + + ax.spy(imat, markersize=markersize, **spy_kwds) ax.tick_params(length=0) if highlight_coarse: start = (0, 0) - for vblocks, cblocks in zip(vpart, cpart): + for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): # Get the total number of variables/constraints in this part # of the coarse partition nv = sum(len(vb) for vb in vblocks) nc = sum(len(cb) for cb in cblocks) stop = (start[0] + nv - 1, start[1] + nc - 1) - ax.add_patch(_get_rectangle_around_coords(start, stop)) + if not (i == 1 and skip_wellconstrained): + # Regardless of whether we are plotting in upper or lower + # triangular order, the well-constrained subsystem is at + # position 1 + ax.add_patch( + _get_rectangle_around_coords(start, stop, linewidth=linewidth) + ) start = (stop[0] + 1, stop[1] + 1) if highlight_fine: + # Use dashed lines to distinguish inner from outer partitions + # if we are highlighting both + linestyle = "--" if highlight_coarse else "-" start = (0, 0) for vb, cb in zip(vpart_fine, cpart_fine): stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) - ax.add_patch(_get_rectangle_around_coords(start, stop)) + ax.add_patch( + _get_rectangle_around_coords( + start, + stop, + linestyle=linestyle, + linewidth=linewidth, + ) + ) start = (stop[0] + 1, stop[1] + 1) return fig, ax From 6464db3587e4c3be7c311ca2cc18d907cd4e3cf0 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:09:35 -0600 Subject: [PATCH 0783/1178] move spy_dulmage_mendelsohn to visualize module --- pyomo/contrib/incidence_analysis/interface.py | 211 +---------------- pyomo/contrib/incidence_analysis/visualize.py | 222 ++++++++++++++++++ 2 files changed, 223 insertions(+), 210 deletions(-) create mode 100644 pyomo/contrib/incidence_analysis/visualize.py diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index a4f08c737ec..50cb84daaf5 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -29,7 +29,7 @@ plotly, ) from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import get_config_from_kwds, IncidenceOrder +from pyomo.contrib.incidence_analysis.config import get_config_from_kwds from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -995,212 +995,3 @@ def add_edge(self, variable, constraint): con_id = self._con_index_map[constraint] self._incidence_graph.add_edge(var_id, con_id) - - def partition_variables_and_constraints( - self, - variables=None, - constraints=None, - order=IncidenceOrder.dulmage_mendelsohn_upper, - ): - """Partition variables and constraints in an incidence graph - """ - variables, constraints = self._validate_input(variables, constraints) - vdmp, cdmp = self.dulmage_mendelsohn(variables=variables, constraints=constraints) - - ucv = vdmp.unmatched + vdmp.underconstrained - ucc = cdmp.underconstrained - - ocv = vdmp.overconstrained - occ = cdmp.overconstrained + cdmp.unmatched - - ucvblocks, uccblocks = self.get_connected_components( - variables=ucv, constraints=ucc - ) - ocvblocks, occblocks = self.get_connected_components( - variables=ocv, constraints=occ - ) - wcvblocks, wccblocks = self.block_triangularize( - variables=vdmp.square, constraints=cdmp.square - ) - # By default, we block-*lower* triangularize. By default, however, we want - # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. - wcvblocks.reverse() - wccblocks.reverse() - vpartition = [ucvblocks, wcvblocks, ocvblocks] - cpartition = [uccblocks, wccblocks, occblocks] - - if order == IncidenceOrder.dulmage_mendelsohn_lower: - # If a block-lower triangular matrix was requested, we need to reverse - # both the inner and outer partitions - vpartition.reverse() - cpartition.reverse() - for vb in vpartition: - vb.reverse() - for cb in cpartition: - cb.reverse() - - return vpartition, cpartition - - -import matplotlib.pyplot as plt -from matplotlib.patches import Rectangle - -def _get_rectangle_around_coords( - ij1, - ij2, - linewidth=2, - linestyle="-", -): - i1, j1 = ij1 - i2, j2 = ij2 - buffer = 0.5 - ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) - width = abs(i1 - i2) + 2*buffer - height = abs(j1 - j2) + 2*buffer - rect = Rectangle( - ll_corner, - width, - height, - clip_on=False, - fill=False, - edgecolor="orange", - linewidth=linewidth, - linestyle=linestyle, - ) - return rect - - -def spy_dulmage_mendelsohn( - model, - *, - incidence_kwds=None, - order=IncidenceOrder.dulmage_mendelsohn_upper, - highlight_coarse=True, - highlight_fine=True, - skip_wellconstrained=False, - ax=None, - linewidth=2, - spy_kwds=None, -): - """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib - axes - - This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting - an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine - partitions highlighted. The coarse partition refers to the under-constrained, - over-constrained, and well-constrained subsystems, while the fine partition - refers to block diagonal or block triangular partitions of the former - subsystems. - - Parameters - ---------- - - model: ``ConcreteModel`` - Input model to plot sparsity structure of - - incidence_kwds: dict, optional - Config options for ``IncidenceGraphInterface`` - - order: ``IncidenceOrder``, optional - Order in which to plot sparsity structure - - highlight_coarse: bool, optional - Whether to draw a rectange around the coarse partition - - highlight_fine: bool, optional - Whether to draw a rectangle around the fine partition - - skip_wellconstrained: bool, optional - Whether to skip highlighting the well-constrained subsystem of the - coarse partition. Default False - - ax: ``matplotlib.pyplot.Axes``, optional - Axes object on which to plot. If not provided, new figure - and axes are created. - - linewidth: int, optional - Line width of for rectangle used to highlight. Default 2 - - spy_kwds: dict, optional - Keyword arguments for ``Axes.spy`` - - Returns - ------- - - fig: ``matplotlib.pyplot.Figure`` or ``None`` - Figure on which the sparsity structure is plotted. ``None`` if axes - are provided - - ax: ``matplotlib.pyplot.Axes`` - Axes on which the sparsity structure is plotted - - """ - if incidence_kwds is None: - incidence_kwds = {} - if spy_kwds is None: - spy_kwds = {} - - igraph = IncidenceGraphInterface(model, **incidence_kwds) - vpart, cpart = igraph.partition_variables_and_constraints(order=order) - vpart_fine = sum(vpart, []) - cpart_fine = sum(cpart, []) - vorder = sum(vpart_fine, []) - corder = sum(cpart_fine, []) - - imat = get_structural_incidence_matrix(vorder, corder) - nvar = len(vorder) - ncon = len(corder) - - if ax is None: - fig, ax = plt.subplots() - else: - fig = None - - markersize = spy_kwds.pop("markersize", None) - if markersize is None: - # At 10000 vars/cons, we want markersize=0.2 - # At 20 vars/cons, we want markersize=10 - # We assume we want a linear relationship between 1/nvar - # and the markersize. - markersize = ( - (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) - + 0.2 - ) - - ax.spy(imat, markersize=markersize, **spy_kwds) - ax.tick_params(length=0) - if highlight_coarse: - start = (0, 0) - for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): - # Get the total number of variables/constraints in this part - # of the coarse partition - nv = sum(len(vb) for vb in vblocks) - nc = sum(len(cb) for cb in cblocks) - stop = (start[0] + nv - 1, start[1] + nc - 1) - if not (i == 1 and skip_wellconstrained): - # Regardless of whether we are plotting in upper or lower - # triangular order, the well-constrained subsystem is at - # position 1 - ax.add_patch( - _get_rectangle_around_coords(start, stop, linewidth=linewidth) - ) - start = (stop[0] + 1, stop[1] + 1) - - if highlight_fine: - # Use dashed lines to distinguish inner from outer partitions - # if we are highlighting both - linestyle = "--" if highlight_coarse else "-" - start = (0, 0) - for vb, cb in zip(vpart_fine, cpart_fine): - stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) - ax.add_patch( - _get_rectangle_around_coords( - start, - stop, - linestyle=linestyle, - linewidth=linewidth, - ) - ) - start = (stop[0] + 1, stop[1] + 1) - - return fig, ax diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py new file mode 100644 index 00000000000..929f8a77d4e --- /dev/null +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -0,0 +1,222 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +"""Module for visualizing results of incidence graph or matrix analysis + +""" +from pyomo.contrib.incidence_analysis.config import IncidenceOrder +from pyomo.contrib.incidence_analysis.interface import ( + IncidenceGraphInterface, + get_structural_incidence_matrix, +) +from pyomo.common.dependencies import matplotlib + + +def _partition_variables_and_constraints( + model, order=IncidenceOrder.dulmage_mendelsohn_upper, **kwds +): + """Partition variables and constraints in an incidence graph + """ + igraph = IncidenceGraphInterface(model, **kwds) + vdmp, cdmp = igraph.dulmage_mendelsohn() + + ucv = vdmp.unmatched + vdmp.underconstrained + ucc = cdmp.underconstrained + + ocv = vdmp.overconstrained + occ = cdmp.overconstrained + cdmp.unmatched + + ucvblocks, uccblocks = igraph.get_connected_components( + variables=ucv, constraints=ucc + ) + ocvblocks, occblocks = igraph.get_connected_components( + variables=ocv, constraints=occ + ) + wcvblocks, wccblocks = igraph.block_triangularize( + variables=vdmp.square, constraints=cdmp.square + ) + # By default, we block-*lower* triangularize. By default, however, we want + # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. + wcvblocks.reverse() + wccblocks.reverse() + vpartition = [ucvblocks, wcvblocks, ocvblocks] + cpartition = [uccblocks, wccblocks, occblocks] + + if order == IncidenceOrder.dulmage_mendelsohn_lower: + # If a block-lower triangular matrix was requested, we need to reverse + # both the inner and outer partitions + vpartition.reverse() + cpartition.reverse() + for vb in vpartition: + vb.reverse() + for cb in cpartition: + cb.reverse() + + return vpartition, cpartition + + +def _get_rectangle_around_coords( + ij1, + ij2, + linewidth=2, + linestyle="-", +): + i1, j1 = ij1 + i2, j2 = ij2 + buffer = 0.5 + ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) + width = abs(i1 - i2) + 2*buffer + height = abs(j1 - j2) + 2*buffer + rect = matplotlib.patches.Rectangle( + ll_corner, + width, + height, + clip_on=False, + fill=False, + edgecolor="orange", + linewidth=linewidth, + linestyle=linestyle, + ) + return rect + + +def spy_dulmage_mendelsohn( + model, + *, + incidence_kwds=None, + order=IncidenceOrder.dulmage_mendelsohn_upper, + highlight_coarse=True, + highlight_fine=True, + skip_wellconstrained=False, + ax=None, + linewidth=2, + spy_kwds=None, +): + """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib axes + + This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting + an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine + partitions highlighted. The coarse partition refers to the under-constrained, + over-constrained, and well-constrained subsystems, while the fine partition + refers to block diagonal or block triangular partitions of the former + subsystems. + + Parameters + ---------- + + model: ``ConcreteModel`` + Input model to plot sparsity structure of + + incidence_kwds: dict, optional + Config options for ``IncidenceGraphInterface`` + + order: ``IncidenceOrder``, optional + Order in which to plot sparsity structure + + highlight_coarse: bool, optional + Whether to draw a rectange around the coarse partition + + highlight_fine: bool, optional + Whether to draw a rectangle around the fine partition + + skip_wellconstrained: bool, optional + Whether to skip highlighting the well-constrained subsystem of the + coarse partition. Default False + + ax: ``matplotlib.pyplot.Axes``, optional + Axes object on which to plot. If not provided, new figure + and axes are created. + + linewidth: int, optional + Line width of for rectangle used to highlight. Default 2 + + spy_kwds: dict, optional + Keyword arguments for ``Axes.spy`` + + Returns + ------- + + fig: ``matplotlib.pyplot.Figure`` or ``None`` + Figure on which the sparsity structure is plotted. ``None`` if axes + are provided + + ax: ``matplotlib.pyplot.Axes`` + Axes on which the sparsity structure is plotted + + """ + plt = matplotlib.pyplot + if incidence_kwds is None: + incidence_kwds = {} + if spy_kwds is None: + spy_kwds = {} + + vpart, cpart = _partition_variables_and_constraints(model, order=order) + vpart_fine = sum(vpart, []) + cpart_fine = sum(cpart, []) + vorder = sum(vpart_fine, []) + corder = sum(cpart_fine, []) + + imat = get_structural_incidence_matrix(vorder, corder) + nvar = len(vorder) + ncon = len(corder) + + if ax is None: + fig, ax = plt.subplots() + else: + fig = None + + markersize = spy_kwds.pop("markersize", None) + if markersize is None: + # At 10000 vars/cons, we want markersize=0.2 + # At 20 vars/cons, we want markersize=10 + # We assume we want a linear relationship between 1/nvar + # and the markersize. + markersize = ( + (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) + + 0.2 + ) + + ax.spy(imat, markersize=markersize, **spy_kwds) + ax.tick_params(length=0) + if highlight_coarse: + start = (0, 0) + for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): + # Get the total number of variables/constraints in this part + # of the coarse partition + nv = sum(len(vb) for vb in vblocks) + nc = sum(len(cb) for cb in cblocks) + stop = (start[0] + nv - 1, start[1] + nc - 1) + if not (i == 1 and skip_wellconstrained): + # Regardless of whether we are plotting in upper or lower + # triangular order, the well-constrained subsystem is at + # position 1 + ax.add_patch( + _get_rectangle_around_coords(start, stop, linewidth=linewidth) + ) + start = (stop[0] + 1, stop[1] + 1) + + if highlight_fine: + # Use dashed lines to distinguish inner from outer partitions + # if we are highlighting both + linestyle = "--" if highlight_coarse else "-" + start = (0, 0) + for vb, cb in zip(vpart_fine, cpart_fine): + stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + ax.add_patch( + _get_rectangle_around_coords( + start, + stop, + linestyle=linestyle, + linewidth=linewidth, + ) + ) + start = (stop[0] + 1, stop[1] + 1) + + return fig, ax From 4f78f7ac1587f717a3a39d7ef90ec58ef02f5c5e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:19:57 -0600 Subject: [PATCH 0784/1178] module to "test" visualization --- .../tests/test_visualize.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pyomo/contrib/incidence_analysis/tests/test_visualize.py diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py new file mode 100644 index 00000000000..ceb36c33e34 --- /dev/null +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -0,0 +1,41 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.common.dependencies import matplotlib, matplotlib_available +from pyomo.contrib.incidence_analysis.visualize import spy_dulmage_mendelsohn +from pyomo.contrib.incidence_analysis.tests.models_for_testing import ( + make_gas_expansion_model, + make_dynamic_model, + make_degenerate_solid_phase_model, +) + + +@unittest.skipUnless(matplotlib_available, "Matplotlib is not available") +class TestSpy(unittest.TestCase): + + def test_spy_dulmage_mendelsohn(self): + models = [ + make_gas_expansion_model(), + make_dynamic_model(), + make_degenerate_solid_phase_model(), + ] + for m in models: + fig, ax = spy_dulmage_mendelsohn(m) + # Note that this is a weak test. We just test that we can call the + # plot method, it doesn't raise an error, and gives us back the + # types we expect. We don't attemt to validate the resulting plot. + self.assertTrue(isinstance(fig, matplotlib.pyplot.Figure)) + self.assertTrue(isinstance(ax, matplotlib.pyplot.Axes)) + + +if __name__ == "__main__": + unittest.main() From d10e549bbaa15a3a50a13f02c5d74f0c94321ea6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:40:49 -0600 Subject: [PATCH 0785/1178] apply black --- .../tests/test_visualize.py | 1 - pyomo/contrib/incidence_analysis/visualize.py | 30 +++++++------------ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py index ceb36c33e34..ea740e86c27 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_visualize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -21,7 +21,6 @@ @unittest.skipUnless(matplotlib_available, "Matplotlib is not available") class TestSpy(unittest.TestCase): - def test_spy_dulmage_mendelsohn(self): models = [ make_gas_expansion_model(), diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 929f8a77d4e..e198d859db5 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -22,8 +22,7 @@ def _partition_variables_and_constraints( model, order=IncidenceOrder.dulmage_mendelsohn_upper, **kwds ): - """Partition variables and constraints in an incidence graph - """ + """Partition variables and constraints in an incidence graph""" igraph = IncidenceGraphInterface(model, **kwds) vdmp, cdmp = igraph.dulmage_mendelsohn() @@ -62,18 +61,13 @@ def _partition_variables_and_constraints( return vpartition, cpartition -def _get_rectangle_around_coords( - ij1, - ij2, - linewidth=2, - linestyle="-", -): +def _get_rectangle_around_coords(ij1, ij2, linewidth=2, linestyle="-"): i1, j1 = ij1 i2, j2 = ij2 buffer = 0.5 - ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) - width = abs(i1 - i2) + 2*buffer - height = abs(j1 - j2) + 2*buffer + ll_corner = (min(i1, i2) - buffer, min(j1, j2) - buffer) + width = abs(i1 - i2) + 2 * buffer + height = abs(j1 - j2) + 2 * buffer rect = matplotlib.patches.Rectangle( ll_corner, width, @@ -136,7 +130,7 @@ def spy_dulmage_mendelsohn( linewidth: int, optional Line width of for rectangle used to highlight. Default 2 - + spy_kwds: dict, optional Keyword arguments for ``Axes.spy`` @@ -178,10 +172,9 @@ def spy_dulmage_mendelsohn( # At 20 vars/cons, we want markersize=10 # We assume we want a linear relationship between 1/nvar # and the markersize. - markersize = ( - (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) - + 0.2 - ) + markersize = (10.0 - 0.2) / (1 / 20 - 1 / 10000) * ( + 1 / max(nvar, ncon) - 1 / 10000 + ) + 0.2 ax.spy(imat, markersize=markersize, **spy_kwds) ax.tick_params(length=0) @@ -211,10 +204,7 @@ def spy_dulmage_mendelsohn( stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) ax.add_patch( _get_rectangle_around_coords( - start, - stop, - linestyle=linestyle, - linewidth=linewidth, + start, stop, linestyle=linestyle, linewidth=linewidth ) ) start = (stop[0] + 1, stop[1] + 1) From 3da70ddcab8322cde137cb782001cfc431af546d Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:46:18 -0600 Subject: [PATCH 0786/1178] fix typos --- pyomo/contrib/incidence_analysis/tests/test_visualize.py | 2 +- pyomo/contrib/incidence_analysis/visualize.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py index ea740e86c27..3a6a403810e 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_visualize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -31,7 +31,7 @@ def test_spy_dulmage_mendelsohn(self): fig, ax = spy_dulmage_mendelsohn(m) # Note that this is a weak test. We just test that we can call the # plot method, it doesn't raise an error, and gives us back the - # types we expect. We don't attemt to validate the resulting plot. + # types we expect. We don't attempt to validate the resulting plot. self.assertTrue(isinstance(fig, matplotlib.pyplot.Figure)) self.assertTrue(isinstance(ax, matplotlib.pyplot.Axes)) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index e198d859db5..05c0661070a 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -115,7 +115,7 @@ def spy_dulmage_mendelsohn( Order in which to plot sparsity structure highlight_coarse: bool, optional - Whether to draw a rectange around the coarse partition + Whether to draw a rectangle around the coarse partition highlight_fine: bool, optional Whether to draw a rectangle around the fine partition From 86bdcab77b1f26df97d94c7616f95e6a38a33626 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 22:24:04 -0600 Subject: [PATCH 0787/1178] skip test if scipy/networkx not available --- pyomo/contrib/incidence_analysis/tests/test_visualize.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py index 3a6a403810e..7c5538b671f 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_visualize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -10,7 +10,12 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.common.dependencies import matplotlib, matplotlib_available +from pyomo.common.dependencies import ( + matplotlib, + matplotlib_available, + scipy_available, + networkx_available, +) from pyomo.contrib.incidence_analysis.visualize import spy_dulmage_mendelsohn from pyomo.contrib.incidence_analysis.tests.models_for_testing import ( make_gas_expansion_model, @@ -20,6 +25,8 @@ @unittest.skipUnless(matplotlib_available, "Matplotlib is not available") +@unittest.skipUnless(scipy_available, "SciPy is not available") +@unittest.skipUnless(networkx_available, "NetworkX is not available") class TestSpy(unittest.TestCase): def test_spy_dulmage_mendelsohn(self): models = [ From a89ef831321118862ad8082f5992886d906fc42b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 08:59:40 -0600 Subject: [PATCH 0788/1178] Bugfix: bound methods don't work on ScalarBlock --- pyomo/contrib/solver/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8840265763e..12cb2e83918 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,11 +14,11 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import Constraint, _GeneralConstraintData +from pyomo.core.base.var import Var, _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning @@ -435,9 +435,9 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - legacy_results.problem.number_of_constraints = model.nconstraints() - legacy_results.problem.number_of_variables = model.nvariables() - number_of_objectives = model.nobjectives() + legacy_results.problem.number_of_constraints = len(list(model.component_map(ctype=Constraint))) + legacy_results.problem.number_of_variables = len(list(model.component_map(ctype=Var))) + number_of_objectives = len(list(model.component_map(ctype=Objective))) legacy_results.problem.number_of_objectives = number_of_objectives if number_of_objectives == 1: obj = get_objective(model) From 617cc59a141a551e1e5d8b65551d7e7c734f4ac8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 10:41:45 -0600 Subject: [PATCH 0789/1178] Apply black --- pyomo/contrib/solver/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 12cb2e83918..91398ba5970 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -435,8 +435,12 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - legacy_results.problem.number_of_constraints = len(list(model.component_map(ctype=Constraint))) - legacy_results.problem.number_of_variables = len(list(model.component_map(ctype=Var))) + legacy_results.problem.number_of_constraints = len( + list(model.component_map(ctype=Constraint)) + ) + legacy_results.problem.number_of_variables = len( + list(model.component_map(ctype=Var)) + ) number_of_objectives = len(list(model.component_map(ctype=Objective))) legacy_results.problem.number_of_objectives = number_of_objectives if number_of_objectives == 1: From 8c2bb52b8142bc2aa824fe20c0336cc8a5c62176 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 11:40:06 -0600 Subject: [PATCH 0790/1178] Update installation documentation to include Cython instructions --- doc/OnlineDocs/installation.rst | 35 ++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/installation.rst b/doc/OnlineDocs/installation.rst index ecba05e13fb..2ed42f35f4e 100644 --- a/doc/OnlineDocs/installation.rst +++ b/doc/OnlineDocs/installation.rst @@ -12,7 +12,7 @@ version, Pyomo will remove testing for that Python version. Using CONDA ~~~~~~~~~~~ -We recommend installation with *conda*, which is included with the +We recommend installation with ``conda``, which is included with the Anaconda distribution of Python. You can install Pyomo in your system Python installation by executing the following in a shell: @@ -21,7 +21,7 @@ Python installation by executing the following in a shell: conda install -c conda-forge pyomo Optimization solvers are not installed with Pyomo, but some open source -optimization solvers can be installed with conda as well: +optimization solvers can be installed with ``conda`` as well: :: @@ -31,7 +31,7 @@ optimization solvers can be installed with conda as well: Using PIP ~~~~~~~~~ -The standard utility for installing Python packages is *pip*. You +The standard utility for installing Python packages is ``pip``. You can install Pyomo in your system Python installation by executing the following in a shell: @@ -43,14 +43,14 @@ the following in a shell: Conditional Dependencies ~~~~~~~~~~~~~~~~~~~~~~~~ -Extensions to Pyomo, and many of the contributions in `pyomo.contrib`, +Extensions to Pyomo, and many of the contributions in ``pyomo.contrib``, often have conditional dependencies on a variety of third-party Python packages including but not limited to: matplotlib, networkx, numpy, openpyxl, pandas, pint, pymysql, pyodbc, pyro4, scipy, sympy, and xlrd. A full list of conditional dependencies can be found in Pyomo's -`setup.py` and displayed using: +``setup.py`` and displayed using: :: @@ -72,3 +72,28 @@ with the standard Anaconda installation. You can check which Python packages you have installed using the command ``conda list`` or ``pip list``. Additional Python packages may be installed as needed. + + +Installation with Cython +~~~~~~~~~~~~~~~~~~~~~~~~ + +Users can opt to install Pyomo with +`cython `_ +initialized. + +.. note:: + This can only be done via ``pip`` or from source. + +Via ``pip``: + +:: + + pip install pyomo --global-option="--with-cython" + +From source: + +:: + + git clone https://github.com/Pyomo/pyomo.git + cd pyomo + python setup.py install --with-cython From e17f27559b01ddd07aed2011e68032289493d8fa Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 12:58:02 -0600 Subject: [PATCH 0791/1178] Add links to Pyomo Book Springer page --- README.md | 1 + doc/OnlineDocs/bibliography.rst | 2 ++ doc/OnlineDocs/tutorial_examples.rst | 17 ++++++++++------- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 12c3ce8ed9a..707f1a06c5a 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ version, we will remove testing for that Python version. ### Tutorials and Examples +* [Pyomo — Optimization Modeling in Python](https://link.springer.com/book/10.1007/978-3-030-68928-5) * [Pyomo Workshop Slides](https://github.com/Pyomo/pyomo-tutorials/blob/main/Pyomo-Workshop-December-2023.pdf) * [Prof. Jeffrey Kantor's Pyomo Cookbook](https://jckantor.github.io/ND-Pyomo-Cookbook/) * The [companion notebooks](https://mobook.github.io/MO-book/intro.html) diff --git a/doc/OnlineDocs/bibliography.rst b/doc/OnlineDocs/bibliography.rst index 6cbb96d3bfb..c12d3f81d8c 100644 --- a/doc/OnlineDocs/bibliography.rst +++ b/doc/OnlineDocs/bibliography.rst @@ -39,6 +39,8 @@ Bibliography John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Vol. 67. Springer, 2021. + doi: `10.1007/978-3-030-68928-5 + `_ .. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. "Pyomo: modeling and solving mathematical programs in diff --git a/doc/OnlineDocs/tutorial_examples.rst b/doc/OnlineDocs/tutorial_examples.rst index 6a40949ef90..a18f9d77d42 100644 --- a/doc/OnlineDocs/tutorial_examples.rst +++ b/doc/OnlineDocs/tutorial_examples.rst @@ -3,15 +3,18 @@ Pyomo Tutorial Examples Additional Pyomo tutorials and examples can be found at the following links: -`Pyomo Workshop Slides and Exercises -`_ +* `Pyomo — Optimization Modeling in Python + `_ ([PyomoBookIII]_) -`Prof. Jeffrey Kantor's Pyomo Cookbook -`_ +* `Pyomo Workshop Slides and Exercises + `_ -The `companion notebooks `_ -for *Hands-On Mathematical Optimization with Python* +* `Prof. Jeffrey Kantor's Pyomo Cookbook + `_ -`Pyomo Gallery `_ +* The `companion notebooks `_ + for *Hands-On Mathematical Optimization with Python* + +* `Pyomo Gallery `_ From 399cbf54cae48803933316e856374adf7ddec766 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 13:04:51 -0600 Subject: [PATCH 0792/1178] Add recommendationg for advanced users --- doc/OnlineDocs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/installation.rst b/doc/OnlineDocs/installation.rst index 2ed42f35f4e..83cd08e7a4a 100644 --- a/doc/OnlineDocs/installation.rst +++ b/doc/OnlineDocs/installation.rst @@ -90,7 +90,7 @@ Via ``pip``: pip install pyomo --global-option="--with-cython" -From source: +From source (recommended for advanced users only): :: From 9b00edd4105fcfbba775a4b85f485aa0cca40d3a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 14:11:58 -0600 Subject: [PATCH 0793/1178] Hack for model.solutions --- pyomo/contrib/solver/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 91398ba5970..c193b2c0789 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -468,7 +468,15 @@ def _solution_handler( """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') - model.solutions.add_symbol_map(symbol_map) + try: + model.solutions.add_symbol_map(symbol_map) + except AttributeError: + # Something wacky happens in IDAES due to the usage of ScalarBlock + # instead of PyomoModel. This is an attempt to fix that. + from pyomo.core.base.PyomoModel import ModelSolutions + + setattr(model.solutions, ModelSolutions()) + model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True if load_solutions: From ac64a5a552a8feede8624ce0a7d952fdea52e2c2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 15:01:32 -0600 Subject: [PATCH 0794/1178] Typo: would be nice if setattr was used correctly --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index c193b2c0789..cdf58416659 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -475,7 +475,7 @@ def _solution_handler( # instead of PyomoModel. This is an attempt to fix that. from pyomo.core.base.PyomoModel import ModelSolutions - setattr(model.solutions, ModelSolutions()) + setattr(model, 'solutions', ModelSolutions()) model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True From ca793605d99036c5513d58dad6d67a31ce2e2eba Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 15:11:55 -0600 Subject: [PATCH 0795/1178] Need an instance in there --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index cdf58416659..07efbaed449 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -475,7 +475,7 @@ def _solution_handler( # instead of PyomoModel. This is an attempt to fix that. from pyomo.core.base.PyomoModel import ModelSolutions - setattr(model, 'solutions', ModelSolutions()) + setattr(model, 'solutions', ModelSolutions(model)) model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True From f1177a287c2cf3dd0c2d9a79818209d49b1c533f Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 25 Mar 2024 16:12:52 -0600 Subject: [PATCH 0796/1178] dont draw box around DM subsystems if they are empty --- pyomo/contrib/incidence_analysis/visualize.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 05c0661070a..8cefbcf61c9 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -186,10 +186,16 @@ def spy_dulmage_mendelsohn( nv = sum(len(vb) for vb in vblocks) nc = sum(len(cb) for cb in cblocks) stop = (start[0] + nv - 1, start[1] + nc - 1) - if not (i == 1 and skip_wellconstrained): + if ( + not (i == 1 and skip_wellconstrained) + and nv > 0 and nc > 0 + ): # Regardless of whether we are plotting in upper or lower # triangular order, the well-constrained subsystem is at # position 1 + # + # The get-rectangle function doesn't look good if we give it + # an "empty region" to box. ax.add_patch( _get_rectangle_around_coords(start, stop, linewidth=linewidth) ) @@ -202,6 +208,7 @@ def spy_dulmage_mendelsohn( start = (0, 0) for vb, cb in zip(vpart_fine, cpart_fine): stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + # Note that the subset's we're boxing here can't be empty. ax.add_patch( _get_rectangle_around_coords( start, stop, linestyle=linestyle, linewidth=linewidth From 6b94db6177424f0a33ebdf14514369ed02ece50e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 25 Mar 2024 17:51:39 -0600 Subject: [PATCH 0797/1178] reformat --- pyomo/contrib/incidence_analysis/visualize.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 8cefbcf61c9..9360d8ddfc6 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -186,10 +186,7 @@ def spy_dulmage_mendelsohn( nv = sum(len(vb) for vb in vblocks) nc = sum(len(cb) for cb in cblocks) stop = (start[0] + nv - 1, start[1] + nc - 1) - if ( - not (i == 1 and skip_wellconstrained) - and nv > 0 and nc > 0 - ): + if not (i == 1 and skip_wellconstrained) and nv > 0 and nc > 0: # Regardless of whether we are plotting in upper or lower # triangular order, the well-constrained subsystem is at # position 1 From 53635a9bbb1817ef2d3e4881779736cdabd87345 Mon Sep 17 00:00:00 2001 From: Eslick Date: Tue, 26 Mar 2024 10:56:25 -0400 Subject: [PATCH 0798/1178] Fix black and substring issue --- pyomo/solvers/plugins/solvers/ASL.py | 3 ++- pyomo/solvers/plugins/solvers/IPOPT.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 3ebe5c3b422..38a9fc1df58 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -160,8 +160,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: + existing = set(env['AMPLFUNC'].split("\n")) for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in env['AMPLFUNC']: + if line not in existing: env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 17b68da6364..8f5190a4a07 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -121,8 +121,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: + existing = set(env['AMPLFUNC'].split("\n")) for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in env['AMPLFUNC']: + if line not in existing: env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] From dd3bb6a3adddec75c432505fac42b6e987bd4bd8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 14:07:05 -0600 Subject: [PATCH 0799/1178] Adding a test for the bug --- pyomo/gdp/tests/test_bigm.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index c6ac49f6d36..79ad24ae782 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -19,8 +19,11 @@ Set, Constraint, ComponentMap, + LogicalConstraint, + Objective, SolverFactory, Suffix, + TerminationCondition, ConcreteModel, Var, Any, @@ -2193,6 +2196,43 @@ def test_decl_order_opposite_instantiation_order(self): def test_do_not_assume_nested_indicators_local(self): ct.check_do_not_assume_nested_indicators_local(self, 'gdp.bigm') + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_constraints_not_enforced_when_an_ancestor_indicator_is_False(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 30)) + + m.left = Disjunct() + m.left.left = Disjunct() + m.left.left.c = Constraint(expr=m.x >= 10) + m.left.right = Disjunct() + m.left.right.c = Constraint(expr=m.x >= 9) + m.left.disjunction = Disjunction(expr=[m.left.left, m.left.right]) + m.right = Disjunct() + m.right.left = Disjunct() + m.right.left.c = Constraint(expr=m.x >= 11) + m.right.right = Disjunct() + m.right.right.c = Constraint(expr=m.x >= 8) + m.right.disjunction = Disjunction(expr=[m.right.left, m.right.right]) + m.disjunction = Disjunction(expr=[m.left, m.right]) + + m.equiv_left = LogicalConstraint(expr=m.left.left.indicator_var.equivalent_to( + m.right.left.indicator_var)) + m.equiv_right = LogicalConstraint(expr=m.left.right.indicator_var.equivalent_to( + m.right.right.indicator_var)) + + m.obj = Objective(expr=m.x) + + TransformationFactory('gdp.bigm').apply_to(m) + results = SolverFactory('gurobi').solve(m) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + self.assertTrue(value(m.right.indicator_var)) + self.assertFalse(value(m.left.indicator_var)) + self.assertTrue(value(m.right.right.indicator_var)) + self.assertFalse(value(m.right.left.indicator_var)) + self.assertTrue(value(m.left.right.indicator_var)) + self.assertAlmostEqual(value(m.x), 8) + class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the From ff67f852c02068f01f4140305383867f8774f75d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 14:31:00 -0600 Subject: [PATCH 0800/1178] Generalizing how nested Constraints are relaxed in bigm so that they aren't enforced if any parent indicator_var is False --- pyomo/gdp/plugins/bigm.py | 28 ++++++++++++++++++---------- pyomo/gdp/plugins/bigm_mixin.py | 9 ++++++--- pyomo/gdp/util.py | 4 ++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 3f450dbbd4f..118fa6935d7 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -217,17 +217,16 @@ def _apply_to_impl(self, instance, **kwds): t, t.index(), bigM, - parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), + gdp_tree, ) # issue warnings about anything that was in the bigM args dict that we # didn't use _warn_for_unused_bigM_args(bigM, self.used_args, logger) - def _transform_disjunctionData( - self, obj, index, bigM, parent_disjunct=None, root_disjunct=None - ): + def _transform_disjunctionData(self, obj, index, bigM, gdp_tree): + parent_disjunct = gdp_tree.parent(obj) + root_disjunct = gdp_tree.root_disjunct(obj) (transBlock, xorConstraint) = self._setup_transform_disjunctionData( obj, root_disjunct ) @@ -236,7 +235,7 @@ def _transform_disjunctionData( or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.binary_indicator_var - self._transform_disjunct(disjunct, bigM, transBlock) + self._transform_disjunct(disjunct, bigM, transBlock, gdp_tree) if obj.xor: xorConstraint[index] = or_expr == 1 @@ -249,7 +248,7 @@ def _transform_disjunctionData( # and deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, bigM, transBlock): + def _transform_disjunct(self, obj, bigM, transBlock, gdp_tree): # We're not using the preprocessed list here, so this could be # inactive. We've already done the error checking in preprocessing, so # we just skip it here. @@ -261,6 +260,12 @@ def _transform_disjunct(self, obj, bigM, transBlock): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) + indicator_expression = 0 + node = obj + while node is not None: + indicator_expression += 1 - node.binary_indicator_var + node = gdp_tree.parent_disjunct(node) + # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big # deal for Hull, as it uses the component_objects / @@ -270,13 +275,15 @@ def _transform_disjunct(self, obj, bigM, transBlock): # comparing the two relaxations. # # Transform each component within this disjunct - self._transform_block_components(obj, obj, bigM, arg_list, suffix_list) + self._transform_block_components(obj, obj, bigM, arg_list, suffix_list, + indicator_expression) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_constraint( - self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list + self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list, + indicator_expression ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() @@ -348,7 +355,8 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map + c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map, + indicator_expression=indicator_expression ) # deactivate because we relaxed diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 510b36b5102..300509d81f8 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -232,7 +232,8 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraint_map + self, c, i, M, indicator_var, newConstraint, constraint_map, + indicator_expression=None ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -244,6 +245,8 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique + if indicator_expression is None: + indicator_expression = 1 - indicator_var if c.lower is not None: if M[0] is None: @@ -251,7 +254,7 @@ def _add_constraint_expressions( "Cannot relax disjunctive constraint '%s' " "because M is not defined." % name ) - M_expr = M[0] * (1 - indicator_var) + M_expr = M[0] * indicator_expression newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'lb'] @@ -263,7 +266,7 @@ def _add_constraint_expressions( "Cannot relax disjunctive constraint '%s' " "because M is not defined." % name ) - M_expr = M[1] * (1 - indicator_var) + M_expr = M[1] * indicator_expression newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'ub'] diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index fe11975954d..a8c6393f0b3 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -144,13 +144,13 @@ def parent(self, u): Arg: u : A node in the tree """ + if u in self._parent: + return self._parent[u] if u not in self._vertices: raise ValueError( "'%s' is not a vertex in the GDP tree. Cannot " "retrieve its parent." % u ) - if u in self._parent: - return self._parent[u] else: return None From e062625ba66012efcbd7ae6b0b4c849cb623456f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 14:53:33 -0600 Subject: [PATCH 0801/1178] Fixing the first couple tests I broke --- pyomo/gdp/tests/test_bigm.py | 66 ++++++++++++++---------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 79ad24ae782..8d0fa8bd633 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -2091,35 +2091,6 @@ def innerIndexed(d, i): m._pyomo_gdp_bigm_reformulation.relaxedDisjuncts, ) - def check_first_disjunct_constraint(self, disj1c, x, ind_var): - self.assertEqual(len(disj1c), 1) - cons = disj1c[0] - self.assertIsNone(cons.lower) - self.assertEqual(cons.upper, 1) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_quadratic()) - self.assertEqual(len(repn.linear_vars), 1) - self.assertEqual(len(repn.quadratic_vars), 4) - ct.check_linear_coef(self, repn, ind_var, 143) - self.assertEqual(repn.constant, -143) - for i in range(1, 5): - ct.check_squared_term_coef(self, repn, x[i], 1) - - def check_second_disjunct_constraint(self, disj2c, x, ind_var): - self.assertEqual(len(disj2c), 1) - cons = disj2c[0] - self.assertIsNone(cons.lower) - self.assertEqual(cons.upper, 1) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_quadratic()) - self.assertEqual(len(repn.linear_vars), 5) - self.assertEqual(len(repn.quadratic_vars), 4) - self.assertEqual(repn.constant, -63) # M = 99, so this is 36 - 99 - ct.check_linear_coef(self, repn, ind_var, 99) - for i in range(1, 5): - ct.check_squared_term_coef(self, repn, x[i], 1) - ct.check_linear_coef(self, repn, x[i], -6) - def simplify_cons(self, cons, leq): visitor = LinearRepnVisitor({}, {}, {}, None) repn = visitor.walk_expression(cons.body) @@ -2145,30 +2116,45 @@ def check_hierarchical_nested_model(self, m, bigm): # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) - self.check_first_disjunct_constraint(disj1c, m.x, m.disj1.binary_indicator_var) + self.assertEqual(len(disj1c), 1) + cons = disj1c[0] + assertExpressionsEqual( + self, + cons.expr, + m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disj1.binary_indicator_var) <= 1.0 + ) disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) - self.check_second_disjunct_constraint( - disj2c, m.x, m.disjunct_block.disj2.binary_indicator_var + self.assertEqual(len(disj2c), 1) + cons = disj2c[0] + cons.pprint() + assertExpressionsEqual( + self, + cons.expr, + (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 ) # inner disjunction constraints innerd1c = bigm.get_transformed_constraints( m.disjunct_block.disj2.disjunction_disjuncts[0].constraint[1] ) - self.check_first_disjunct_constraint( - innerd1c, - m.x, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, + self.assertEqual(len(innerd1c), 1) + cons = innerd1c[0] + assertExpressionsEqual( + self, + cons.expr, + m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 ) innerd2c = bigm.get_transformed_constraints( m.disjunct_block.disj2.disjunction_disjuncts[1].constraint[1] ) - self.check_second_disjunct_constraint( - innerd2c, - m.x, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, + self.assertEqual(len(innerd2c), 1) + cons = innerd2c[0] + assertExpressionsEqual( + self, + cons.expr, + (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 ) def test_hierarchical_badly_ordered_targets(self): From 01f7ebe58af5ded0325b028c462906d8aa2c4179 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 26 Mar 2024 16:14:05 -0600 Subject: [PATCH 0802/1178] require variables and constraints to be specified separately in `remove_nodes`; update to raise error on invalid components --- pyomo/contrib/incidence_analysis/interface.py | 55 +++++++++++++++---- .../tests/test_interface.py | 33 +++++++---- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 50cb84daaf5..0ed9b34b0f8 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -453,11 +453,29 @@ def _validate_input(self, variables, constraints): raise ValueError("Neither variables nor a model have been provided.") else: variables = self.variables + elif self._incidence_graph is not None: + # If variables were provided and an incidence graph is cached, + # make sure the provided variables exist in the graph. + for var in variables: + if var not in self._var_index_map: + raise KeyError( + f"Variable {var} does not exist in the cached" + " incidence graph." + ) if constraints is None: if self._incidence_graph is None: raise ValueError("Neither constraints nor a model have been provided.") else: constraints = self.constraints + elif self._incidence_graph is not None: + # If constraints were provided and an incidence graph is cached, + # make sure the provided constraints exist in the graph. + for con in constraints: + if con not in self._con_index_map: + raise KeyError( + f"Constraint {con} does not exist in the cached" + " incidence graph." + ) _check_unindexed(variables + constraints) return variables, constraints @@ -854,7 +872,7 @@ def dulmage_mendelsohn(self, variables=None, constraints=None): # Hopefully this does not get too confusing... return var_partition, con_partition - def remove_nodes(self, nodes, constraints=None): + def remove_nodes(self, variables=None, constraints=None): """Removes the specified variables and constraints (columns and rows) from the cached incidence matrix. @@ -866,35 +884,48 @@ def remove_nodes(self, nodes, constraints=None): Parameters ---------- - nodes: list - VarData or ConData objects whose columns or rows will be - removed from the incidence matrix. + variables: list + VarData objects whose nodes will be removed from the incidence graph constraints: list - VarData or ConData objects whose columns or rows will be - removed from the incidence matrix. + ConData objects whose nodes will be removed from the incidence graph + + .. note:: + + **Breaking change in Pyomo vTBD** + + The pre-TBD implementation of ``remove_nodes`` allowed variables and + constraints to remove to be specified in a single list. This made + error checking difficult, and indeed, if invalid components were + provided, we carried on silently instead of throwing an error or + warning. As part of a fix to raise an error if an invalid component + (one that is not part of the incidence graph) is provided, we now require + variables and constraints to be specified separately. """ if constraints is None: constraints = [] + if variables is None: + variables = [] if self._incidence_graph is None: raise RuntimeError( "Attempting to remove variables and constraints from cached " "incidence matrix,\nbut no incidence matrix has been cached." ) - to_exclude = ComponentSet(nodes) - to_exclude.update(constraints) - vars_to_include = [v for v in self.variables if v not in to_exclude] - cons_to_include = [c for c in self.constraints if c not in to_exclude] + variables, constraints = self._validate_input(variables, constraints) + v_exclude = ComponentSet(variables) + c_exclude = ComponentSet(constraints) + vars_to_include = [v for v in self.variables if v not in v_exclude] + cons_to_include = [c for c in self.constraints if c not in c_exclude] incidence_graph = self._extract_subgraph(vars_to_include, cons_to_include) # update attributes self._variables = vars_to_include self._constraints = cons_to_include self._incidence_graph = incidence_graph self._var_index_map = ComponentMap( - (var, i) for i, var in enumerate(self.variables) + (var, i) for i, var in enumerate(vars_to_include) ) self._con_index_map = ComponentMap( - (con, i) for i, con in enumerate(self._constraints) + (con, i) for i, con in enumerate(cons_to_include) ) def plot(self, variables=None, constraints=None, title=None, show=True): diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 4b77d60d8ba..3b2439ed2af 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -634,17 +634,15 @@ def test_exception(self): nlp = PyomoNLP(model) igraph = IncidenceGraphInterface(nlp) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) @unittest.skipUnless(networkx_available, "networkx is not available.") @@ -885,17 +883,15 @@ def test_exception(self): model = make_gas_expansion_model() igraph = IncidenceGraphInterface(model) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) @unittest.skipUnless(scipy_available, "scipy is not available.") def test_remove(self): @@ -923,7 +919,7 @@ def test_remove(self): # Say we know that these variables and constraints should # be matched... vars_to_remove = [model.F[0], model.F[2]] - cons_to_remove = (model.mbal[1], model.mbal[2]) + cons_to_remove = [model.mbal[1], model.mbal[2]] igraph.remove_nodes(vars_to_remove, cons_to_remove) variable_set = ComponentSet(igraph.variables) self.assertNotIn(model.F[0], variable_set) @@ -1309,7 +1305,7 @@ def test_remove(self): # matrix. vars_to_remove = [m.flow_comp[1]] cons_to_remove = [m.flow_eqn[1]] - igraph.remove_nodes(vars_to_remove + cons_to_remove) + igraph.remove_nodes(vars_to_remove, cons_to_remove) var_dmp, con_dmp = igraph.dulmage_mendelsohn() var_con_set = ComponentSet(igraph.variables + igraph.constraints) underconstrained_set = ComponentSet( @@ -1460,6 +1456,21 @@ def test_remove_no_matrix(self): with self.assertRaisesRegex(RuntimeError, "no incidence matrix"): igraph.remove_nodes([m.v1]) + def test_remove_bad_node(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.eq = pyo.Constraint(pyo.PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == m.x[3] + m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3] + igraph = IncidenceGraphInterface(m) + with self.assertRaisesRegex(KeyError, "does not exist"): + # Suppose we think something like this should work. We should get + # an error, and not silently do nothing. + igraph.remove_nodes([m.x], [m.eq]) + + with self.assertRaisesRegex(KeyError, "does not exist"): + igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) + @unittest.skipUnless(networkx_available, "networkx is not available.") @unittest.skipUnless(scipy_available, "scipy is not available.") @@ -1840,7 +1851,7 @@ def test_var_elim(self): for adj_con in igraph.get_adjacent_to(m.x[1]): for adj_var in igraph.get_adjacent_to(m.eq4): igraph.add_edge(adj_var, adj_con) - igraph.remove_nodes([m.x[1], m.eq4]) + igraph.remove_nodes([m.x[1]], [m.eq4]) assert ComponentSet(igraph.variables) == ComponentSet([m.x[2], m.x[3], m.x[4]]) assert ComponentSet(igraph.constraints) == ComponentSet([m.eq1, m.eq2, m.eq3]) From 5ae3cf4a90dceb97cb1f707ccfa76754e783f9f8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 20:58:24 -0600 Subject: [PATCH 0803/1178] Fixing the last test that I broke --- pyomo/gdp/tests/test_bigm.py | 56 +++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 8d0fa8bd633..95c4652e387 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1882,12 +1882,11 @@ def test_m_value_mappings(self): # many of the transformed constraints look like this, so can call this # function to test them. def check_bigM_constraint(self, cons, variable, M, indicator_var): - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, -M) - self.assertEqual(len(repn.linear_vars), 2) - ct.check_linear_coef(self, repn, variable, 1) - ct.check_linear_coef(self, repn, indicator_var, M) + assertExpressionsEqual( + self, + cons.body, + variable - float(M) * (1 - indicator_var.get_associated_binary()) + ) def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): inner_xor = inner_disjunction.algebraic_constraint @@ -1952,6 +1951,14 @@ def test_transformed_constraints(self): .binary_indicator_var, ) ), + 1, + EXPR.MonomialTermExpression( + ( + -1, + m.disjunct[1] + .binary_indicator_var, + ) + ), ] ), ) @@ -1961,37 +1968,41 @@ def test_transformed_constraints(self): ] ), ) - self.assertIsNone(cons1ub.lower) - self.assertEqual(cons1ub.upper, 0) - self.check_bigM_constraint( - cons1ub, m.z, 10, m.disjunct[1].innerdisjunct[0].indicator_var + assertExpressionsEqual( + self, + cons1ub.expr, + m.z - 10.0*(1 - m.disjunct[1].innerdisjunct[0].binary_indicator_var + + 1 - m.disjunct[1].binary_indicator_var) <= 0.0 ) cons2 = bigm.get_transformed_constraints(m.disjunct[1].innerdisjunct[1].c) self.assertEqual(len(cons2), 1) cons2lb = cons2[0] - self.assertEqual(cons2lb.lower, 5) - self.assertIsNone(cons2lb.upper) - self.check_bigM_constraint( - cons2lb, m.z, -5, m.disjunct[1].innerdisjunct[1].indicator_var + assertExpressionsEqual( + self, + cons2lb.expr, + 5.0 <= m.z - (-5.0)*(1 - m.disjunct[1].innerdisjunct[1].binary_indicator_var + + 1 - m.disjunct[1].binary_indicator_var) ) cons3 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct0.c) self.assertEqual(len(cons3), 1) cons3ub = cons3[0] - self.assertEqual(cons3ub.upper, 2) - self.assertIsNone(cons3ub.lower) - self.check_bigM_constraint( - cons3ub, m.x, 7, m.simpledisjunct.innerdisjunct0.indicator_var + assertExpressionsEqual( + self, + cons3ub.expr, + m.x - 7.0*(1 - m.simpledisjunct.innerdisjunct0.binary_indicator_var + 1 - + m.simpledisjunct.binary_indicator_var) <= 2.0 ) cons4 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct1.c) self.assertEqual(len(cons4), 1) cons4lb = cons4[0] - self.assertEqual(cons4lb.lower, 4) - self.assertIsNone(cons4lb.upper) - self.check_bigM_constraint( - cons4lb, m.x, -13, m.simpledisjunct.innerdisjunct1.indicator_var + assertExpressionsEqual( + self, + cons4lb.expr, + m.x - (-13.0)*(1 - m.simpledisjunct.innerdisjunct1.binary_indicator_var + + 1 - m.simpledisjunct.binary_indicator_var) >= 4.0 ) # Here we check that the xor constraint from @@ -2127,7 +2138,6 @@ def check_hierarchical_nested_model(self, m, bigm): disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) self.assertEqual(len(disj2c), 1) cons = disj2c[0] - cons.pprint() assertExpressionsEqual( self, cons.expr, From 0c25598b30c3150851c3434d703aaa929c559911 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 21:00:19 -0600 Subject: [PATCH 0804/1178] black --- pyomo/gdp/plugins/bigm.py | 30 +++++---- pyomo/gdp/plugins/bigm_mixin.py | 10 ++- pyomo/gdp/tests/test_bigm.py | 107 +++++++++++++++++++++++++------- 3 files changed, 109 insertions(+), 38 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 118fa6935d7..d715d913db8 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -213,12 +213,7 @@ def _apply_to_impl(self, instance, **kwds): bigM = self._config.bigM for t in preprocessed_targets: if t.ctype is Disjunction: - self._transform_disjunctionData( - t, - t.index(), - bigM, - gdp_tree, - ) + self._transform_disjunctionData(t, t.index(), bigM, gdp_tree) # issue warnings about anything that was in the bigM args dict that we # didn't use @@ -275,15 +270,21 @@ def _transform_disjunct(self, obj, bigM, transBlock, gdp_tree): # comparing the two relaxations. # # Transform each component within this disjunct - self._transform_block_components(obj, obj, bigM, arg_list, suffix_list, - indicator_expression) + self._transform_block_components( + obj, obj, bigM, arg_list, suffix_list, indicator_expression + ) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_constraint( - self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list, - indicator_expression + self, + obj, + disjunct, + bigMargs, + arg_list, + disjunct_suffix_list, + indicator_expression, ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() @@ -355,8 +356,13 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map, - indicator_expression=indicator_expression + c, + i, + M, + disjunct.binary_indicator_var, + newConstraint, + constraint_map, + indicator_expression=indicator_expression, ) # deactivate because we relaxed diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 300509d81f8..1c3fcb2c64a 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -232,8 +232,14 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraint_map, - indicator_expression=None + self, + c, + i, + M, + indicator_var, + newConstraint, + constraint_map, + indicator_expression=None, ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 95c4652e387..3174a95292e 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1885,7 +1885,7 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): assertExpressionsEqual( self, cons.body, - variable - float(M) * (1 - indicator_var.get_associated_binary()) + variable - float(M) * (1 - indicator_var.get_associated_binary()), ) def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): @@ -1953,11 +1953,7 @@ def test_transformed_constraints(self): ), 1, EXPR.MonomialTermExpression( - ( - -1, - m.disjunct[1] - .binary_indicator_var, - ) + (-1, m.disjunct[1].binary_indicator_var) ), ] ), @@ -1971,8 +1967,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons1ub.expr, - m.z - 10.0*(1 - m.disjunct[1].innerdisjunct[0].binary_indicator_var + - 1 - m.disjunct[1].binary_indicator_var) <= 0.0 + m.z + - 10.0 + * ( + 1 + - m.disjunct[1].innerdisjunct[0].binary_indicator_var + + 1 + - m.disjunct[1].binary_indicator_var + ) + <= 0.0, ) cons2 = bigm.get_transformed_constraints(m.disjunct[1].innerdisjunct[1].c) @@ -1981,8 +1984,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons2lb.expr, - 5.0 <= m.z - (-5.0)*(1 - m.disjunct[1].innerdisjunct[1].binary_indicator_var - + 1 - m.disjunct[1].binary_indicator_var) + 5.0 + <= m.z + - (-5.0) + * ( + 1 + - m.disjunct[1].innerdisjunct[1].binary_indicator_var + + 1 + - m.disjunct[1].binary_indicator_var + ), ) cons3 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct0.c) @@ -1991,8 +2001,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons3ub.expr, - m.x - 7.0*(1 - m.simpledisjunct.innerdisjunct0.binary_indicator_var + 1 - - m.simpledisjunct.binary_indicator_var) <= 2.0 + m.x + - 7.0 + * ( + 1 + - m.simpledisjunct.innerdisjunct0.binary_indicator_var + + 1 + - m.simpledisjunct.binary_indicator_var + ) + <= 2.0, ) cons4 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct1.c) @@ -2001,8 +2018,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons4lb.expr, - m.x - (-13.0)*(1 - m.simpledisjunct.innerdisjunct1.binary_indicator_var - + 1 - m.simpledisjunct.binary_indicator_var) >= 4.0 + m.x + - (-13.0) + * ( + 1 + - m.simpledisjunct.innerdisjunct1.binary_indicator_var + + 1 + - m.simpledisjunct.binary_indicator_var + ) + >= 4.0, ) # Here we check that the xor constraint from @@ -2132,7 +2156,12 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disj1.binary_indicator_var) <= 1.0 + m.x[1] ** 2 + + m.x[2] ** 2 + + m.x[3] ** 2 + + m.x[4] ** 2 + - 143.0 * (1 - m.disj1.binary_indicator_var) + <= 1.0, ) disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) @@ -2141,7 +2170,12 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 + (3 - m.x[1]) ** 2 + + (3 - m.x[2]) ** 2 + + (3 - m.x[3]) ** 2 + + (3 - m.x[4]) ** 2 + - 99.0 * (1 - m.disjunct_block.disj2.binary_indicator_var) + <= 1.0, ) # inner disjunction constraints @@ -2153,7 +2187,18 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 + m.x[1] ** 2 + + m.x[2] ** 2 + + m.x[3] ** 2 + + m.x[4] ** 2 + - 143.0 + * ( + 1 + - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + + 1 + - m.disjunct_block.disj2.binary_indicator_var + ) + <= 1.0, ) innerd2c = bigm.get_transformed_constraints( @@ -2164,7 +2209,18 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 + (3 - m.x[1]) ** 2 + + (3 - m.x[2]) ** 2 + + (3 - m.x[3]) ** 2 + + (3 - m.x[4]) ** 2 + - 99.0 + * ( + 1 + - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + + 1 + - m.disjunct_block.disj2.binary_indicator_var + ) + <= 1.0, ) def test_hierarchical_badly_ordered_targets(self): @@ -2211,17 +2267,20 @@ def test_constraints_not_enforced_when_an_ancestor_indicator_is_False(self): m.right.disjunction = Disjunction(expr=[m.right.left, m.right.right]) m.disjunction = Disjunction(expr=[m.left, m.right]) - m.equiv_left = LogicalConstraint(expr=m.left.left.indicator_var.equivalent_to( - m.right.left.indicator_var)) - m.equiv_right = LogicalConstraint(expr=m.left.right.indicator_var.equivalent_to( - m.right.right.indicator_var)) + m.equiv_left = LogicalConstraint( + expr=m.left.left.indicator_var.equivalent_to(m.right.left.indicator_var) + ) + m.equiv_right = LogicalConstraint( + expr=m.left.right.indicator_var.equivalent_to(m.right.right.indicator_var) + ) m.obj = Objective(expr=m.x) TransformationFactory('gdp.bigm').apply_to(m) results = SolverFactory('gurobi').solve(m) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) self.assertTrue(value(m.right.indicator_var)) self.assertFalse(value(m.left.indicator_var)) self.assertTrue(value(m.right.right.indicator_var)) From fe4a4e0815ac425bab389abd2d44fac5d91acf91 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 21:33:16 -0600 Subject: [PATCH 0805/1178] Updating GDPopt call to _transform_constraint --- pyomo/contrib/gdpopt/util.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/util.py b/pyomo/contrib/gdpopt/util.py index 2cb70f0ea60..babe0245d57 100644 --- a/pyomo/contrib/gdpopt/util.py +++ b/pyomo/contrib/gdpopt/util.py @@ -553,6 +553,13 @@ def _add_bigm_constraint_to_transformed_model(m, constraint, block): # making a Reference to the ComponentData so that it will look like an # indexed component for now. If I redesign bigm at some point, then this # could be prettier. - bigm._transform_constraint(Reference(constraint), parent_disjunct, None, [], []) + bigm._transform_constraint( + Reference(constraint), + parent_disjunct, + None, + [], + [], + 1 - parent_disjunct.binary_indicator_var, + ) # Now get rid of it because this is a class attribute! del bigm._config From 6e1d351126e5e3f0b775d678b1778ceb38de5938 Mon Sep 17 00:00:00 2001 From: Eslick Date: Wed, 27 Mar 2024 08:18:07 -0400 Subject: [PATCH 0806/1178] Add Robbybp's patch --- pyomo/contrib/pynumero/interfaces/pyomo_nlp.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index 51edd09311a..ce148f50ecf 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -92,15 +92,13 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - amplfunc = "\n".join( - filter( - None, - ( - os.environ.get('AMPLFUNC', None), - os.environ.get('PYOMO_AMPLFUNC', None), - ), - ) - ) + amplfunc_lines = os.environ.get("AMPLFUNC", "").split("\n") + existing = set(amplfunc_lines) + for line in os.environ.get("PYOMO_AMPLFUNC", "").split("\n"): + # Skip (a) empty lines and (b) lines we already have + if line != "" and line not in existing: + amplfunc_lines.append(line) + amplfunc = "\n".join(amplfunc_lines) with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) From 0e7fa12f6915e7eacfbaa24b1a6d36af8d46dc3c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 07:09:26 -0600 Subject: [PATCH 0807/1178] Adding some test skips that whatever partial environment I'm living in this morning caught --- pyomo/contrib/gdpopt/tests/test_LBB.py | 1 + pyomo/contrib/gdpopt/tests/test_gdpopt.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/tests/test_LBB.py b/pyomo/contrib/gdpopt/tests/test_LBB.py index 273327b02a4..8a553398fa6 100644 --- a/pyomo/contrib/gdpopt/tests/test_LBB.py +++ b/pyomo/contrib/gdpopt/tests/test_LBB.py @@ -59,6 +59,7 @@ def test_infeasible_GDP(self): self.assertIsNone(m.d.disjuncts[0].indicator_var.value) self.assertIsNone(m.d.disjuncts[1].indicator_var.value) + @unittest.skipUnless(z3_available, "Z3 SAT solver is not available") def test_infeasible_GDP_check_sat(self): """Test for infeasible GDP with check_sat option True.""" m = ConcreteModel() diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 005df56ced5..98750f6e78a 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -1050,7 +1050,8 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(Gurobi().available(), "APPSI Gurobi solver is not available") + @unittest.skipUnless(Gurobi().available() and Gurobi().license_is_valid(), + "APPSI Gurobi solver is not available") def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() From 897704ded324a13bd946ce7191597c5d65d21f1d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 08:39:45 -0600 Subject: [PATCH 0808/1178] Fixing license check, though that's not the real issue --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 98750f6e78a..c33e0172def 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -22,6 +22,7 @@ from pyomo.common.collections import Bunch from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR +from pyomo.contrib.appsi.base import Solver from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.gdpopt.create_oa_subproblems import ( add_util_block, @@ -1050,8 +1051,9 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(Gurobi().available() and Gurobi().license_is_valid(), - "APPSI Gurobi solver is not available") + @unittest.skipUnless(SolverFactory('appsi_gurobi').available( + exception_flag=False) and SolverFactory('appsi_gurobi').license_is_valid(), + "Legacy APPSI Gurobi solver is not available") def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() From 60acf1c2f807f449ae822b43989884fdff41ba00 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 08:40:30 -0600 Subject: [PATCH 0809/1178] black --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index c33e0172def..bf295897ec0 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -1051,9 +1051,11 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(SolverFactory('appsi_gurobi').available( - exception_flag=False) and SolverFactory('appsi_gurobi').license_is_valid(), - "Legacy APPSI Gurobi solver is not available") + @unittest.skipUnless( + SolverFactory('appsi_gurobi').available(exception_flag=False) + and SolverFactory('appsi_gurobi').license_is_valid(), + "Legacy APPSI Gurobi solver is not available", + ) def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() From bb3fafb6231118f64b0e66a95ac74c5db6c11f9f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 09:07:40 -0600 Subject: [PATCH 0810/1178] Debugging GH Actions failures --- pyomo/contrib/gdpopt/solve_subproblem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/gdpopt/solve_subproblem.py b/pyomo/contrib/gdpopt/solve_subproblem.py index e3980c3c784..6ae3cc8e244 100644 --- a/pyomo/contrib/gdpopt/solve_subproblem.py +++ b/pyomo/contrib/gdpopt/solve_subproblem.py @@ -46,6 +46,8 @@ def configure_and_call_solver(model, solver, args, problem_type, timing, time_li solver_args.get('time_limit', float('inf')), remaining ) try: + ## DEBUG + solver_args['tee'] = True results = opt.solve(model, **solver_args) except ValueError as err: if 'Cannot load a SolverResults object with bad status: error' in str(err): From 53684194112384adbb524faa6a36f8077cbc8745 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 11:29:10 -0600 Subject: [PATCH 0811/1178] Skipping 8PP logical problem tests when we don't have a baron license--the transformation of the logical stuff has nested structures and so grew to beyond demo size --- pyomo/contrib/gdpopt/solve_subproblem.py | 2 -- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 9 +++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/solve_subproblem.py b/pyomo/contrib/gdpopt/solve_subproblem.py index 6ae3cc8e244..e3980c3c784 100644 --- a/pyomo/contrib/gdpopt/solve_subproblem.py +++ b/pyomo/contrib/gdpopt/solve_subproblem.py @@ -46,8 +46,6 @@ def configure_and_call_solver(model, solver, args, problem_type, timing, time_li solver_args.get('time_limit', float('inf')), remaining ) try: - ## DEBUG - solver_args['tee'] = True results = opt.solve(model, **solver_args) except ValueError as err: if 'Cannot load a SolverResults object with bad status: error' in str(err): diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index bf295897ec0..9fe8e450cba 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -768,6 +768,9 @@ def test_time_limit(self): results.solver.termination_condition, TerminationCondition.maxTimeLimit ) + @unittest.skipUnless( + license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + ) def test_LOA_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) @@ -871,6 +874,9 @@ def test_LOA_8PP_maxBinary(self): ) ct.check_8PP_solution(self, eight_process, results) + @unittest.skipUnless( + license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + ) def test_LOA_8PP_logical_maxBinary(self): """Test logic-based OA with max_binary initialization.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) @@ -1131,6 +1137,9 @@ def test_RIC_8PP_default_init(self): ) ct.check_8PP_solution(self, eight_process, results) + @unittest.skipUnless( + license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + ) def test_RIC_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) From 096bf542a903b8f232bea240be8028f2694d44de Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 11:42:20 -0600 Subject: [PATCH 0812/1178] whoops, I can read and type and stuff --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 9fe8e450cba..3ac532116aa 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -769,7 +769,7 @@ def test_time_limit(self): ) @unittest.skipUnless( - license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + license_available, "No BARON license--8PP logical problem exceeds demo size" ) def test_LOA_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" @@ -875,7 +875,7 @@ def test_LOA_8PP_maxBinary(self): ct.check_8PP_solution(self, eight_process, results) @unittest.skipUnless( - license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + license_available, "No BARON license--8PP logical problem exceeds demo size" ) def test_LOA_8PP_logical_maxBinary(self): """Test logic-based OA with max_binary initialization.""" @@ -1138,7 +1138,7 @@ def test_RIC_8PP_default_init(self): ct.check_8PP_solution(self, eight_process, results) @unittest.skipUnless( - license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + license_available, "No BARON license--8PP logical problem exceeds demo size" ) def test_RIC_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" From 065b422803f91a929e403df5357cd91d85c9e7c7 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 27 Mar 2024 17:48:28 -0600 Subject: [PATCH 0813/1178] update docstring --- pyomo/contrib/incidence_analysis/visualize.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 9360d8ddfc6..af1bdbbb918 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -112,13 +112,16 @@ def spy_dulmage_mendelsohn( Config options for ``IncidenceGraphInterface`` order: ``IncidenceOrder``, optional - Order in which to plot sparsity structure + Order in which to plot sparsity structure. Default is + ``IncidenceOrder.dulmage_mendelsohn_upper`` for a block-upper triangular + matrix. Set to ``IncidenceOrder.dulmage_mendelsohn_lower`` for a + block-lower triangular matrix. highlight_coarse: bool, optional - Whether to draw a rectangle around the coarse partition + Whether to draw a rectangle around the coarse partition. Default True highlight_fine: bool, optional - Whether to draw a rectangle around the fine partition + Whether to draw a rectangle around the fine partition. Default True skip_wellconstrained: bool, optional Whether to skip highlighting the well-constrained subsystem of the From 81245460c156f187ead1baafaa58195c21389e60 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 27 Mar 2024 20:31:17 -0400 Subject: [PATCH 0814/1178] add highs version check and load_solutions attributes --- pyomo/contrib/mindtpy/algorithm_base_class.py | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 8d25f3c1d3a..0394110675d 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -152,7 +152,9 @@ def __init__(self, **kwds): # Store the OA cuts generated in the mip_start_process. self.mip_start_lazy_oa_cuts = [] # Whether to load solutions in solve() function - self.load_solutions = True + self.mip_load_solutions = True + self.nlp_load_solutions = True + self.regularization_mip_load_solutions = True # Support use as a context manager under current solver API def __enter__(self): @@ -302,7 +304,7 @@ def model_is_valid(self): results = self.mip_opt.solve( self.original_model, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(results.solution) > 0: @@ -846,7 +848,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -868,7 +870,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -999,7 +1001,10 @@ def init_max_binaries(self): mip_args = dict(config.mip_solver_args) update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) results = self.mip_opt.solve( - m, tee=config.mip_solver_tee, load_solutions=self.load_solutions, **mip_args + m, + tee=config.mip_solver_tee, + load_solutions=self.mip_load_solutions, + **mip_args, ) if len(results.solution) > 0: m.solutions.load_from(results) @@ -1119,7 +1124,7 @@ def solve_subproblem(self): results = self.nlp_opt.solve( self.fixed_nlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -1586,7 +1591,7 @@ def fix_dual_bound(self, last_iter_cuts): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) if len(main_mip_results.solution) > 0: @@ -1674,7 +1679,7 @@ def solve_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1735,7 +1740,7 @@ def solve_fp_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1778,7 +1783,7 @@ def solve_regularization_main(self): main_mip_results = self.regularization_mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.regularization_mip_load_solutions, **dict(config.mip_solver_args), ) if len(main_mip_results.solution) > 0: @@ -1994,7 +1999,7 @@ def handle_main_unbounded(self, main_mip): main_mip_results = self.mip_opt.solve( main_mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(main_mip_results.solution) > 0: @@ -2277,6 +2282,11 @@ def check_subsolver_validity(self): raise ValueError(self.config.mip_solver + ' is not available.') if not self.mip_opt.license_is_valid(): raise ValueError(self.config.mip_solver + ' is not licensed.') + if self.config.mip_solver == "appsi_highs": + if self.mip_opt.version() < (1, 7, 0): + raise ValueError( + "MindtPy requires the use of HIGHS version 1.7.0 or higher for full compatibility." + ) if not self.nlp_opt.available(): raise ValueError(self.config.nlp_solver + ' is not available.') if not self.nlp_opt.license_is_valid(): @@ -2324,15 +2334,15 @@ def check_config(self): config.mip_solver = 'cplex_persistent' # related to https://github.com/Pyomo/pyomo/issues/2363 + if 'appsi' in config.mip_solver: + self.mip_load_solutions = False + if 'appsi' in config.nlp_solver: + self.nlp_load_solutions = False if ( - 'appsi' in config.mip_solver - or 'appsi' in config.nlp_solver - or ( - config.mip_regularization_solver is not None - and 'appsi' in config.mip_regularization_solver - ) + config.mip_regularization_solver is not None + and 'appsi' in config.mip_regularization_solver ): - self.load_solutions = False + self.regularization_mip_load_solutions = False ################################################################################################################################ # Feasibility Pump @@ -2400,7 +2410,7 @@ def solve_fp_subproblem(self): results = self.nlp_opt.solve( fp_nlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: From 34c2c36c35260e8207a5850edcc5a985e8b3d55d Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 27 Mar 2024 20:39:26 -0400 Subject: [PATCH 0815/1178] add version check for highs in tests --- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 7 ++++++- pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py | 8 +++++++- pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py | 8 +++++++- pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py | 9 ++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index 27c57370ba2..d0364378ed8 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -56,7 +56,12 @@ QCP_model._generate_model() extreme_model_list = [LP_model.model, QCP_model.model] -required_solvers = ('ipopt', 'appsi_highs') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index fb78be6b2f1..dda0f74147e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -23,7 +23,13 @@ from pyomo.environ import SolverFactory, value from pyomo.opt import TerminationCondition -required_solvers = ('ipopt', 'appsi_highs') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index b8f889e6920..0baa361910e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -28,7 +28,13 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'appsi_highs') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py index d50a41ad000..e01558d48ef 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py @@ -18,7 +18,14 @@ from pyomo.contrib.mindtpy.tests.MINLP_simple import SimpleMINLP as SimpleMINLP model_list = [SimpleMINLP(grey_box=True)] -required_solvers = ('cyipopt', 'glpk') + +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('cyipopt', 'appsi_highs') +else: + required_solvers = ('cyipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: From bd475d1a45ae34fff431694f74cb5f0d7b934535 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 28 Mar 2024 11:34:26 -0400 Subject: [PATCH 0816/1178] Fix docstring typo --- pyomo/contrib/pyros/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 306141e9829..5d386240609 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -272,7 +272,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): option. However, this may be overridden by any user specifications included in a GAMS optfile, which may be difficult to track down. - (3) To ensure the time limit is specified to a strictly + (4) To ensure the time limit is specified to a strictly positive value, the time limit is adjusted to a value of at least 1 second. """ From a99277df9afaad4fd2011376dfeb4a59acc50898 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 10:59:48 -0600 Subject: [PATCH 0817/1178] Allow multiple definitions of solver options --- pyomo/contrib/solver/base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 07efbaed449..7e93bacd54b 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -348,9 +348,9 @@ class LegacySolverWrapper: interface. Necessary for backwards compatibility. """ - def __init__(self, solver_io=None, **kwargs): - if solver_io is not None: - raise NotImplementedError('Still working on this') + def __init__(self, **kwargs): + if 'options' in kwargs: + self.options = kwargs.pop('options') super().__init__(**kwargs) # @@ -393,8 +393,14 @@ def _map_config( self.config.time_limit = timelimit if report_timing is not NOTSET: self.config.report_timing = report_timing + if hasattr(self, 'options'): + self.config.solver_options.set_value(self.options) if options is not NOTSET: + # This block is trying to mimic the existing logic in the legacy + # interface that allows users to pass initialized options to + # the solver object and override them in the solve call. self.config.solver_options.set_value(options) + # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" if raise_exception_on_nonoptimal_result is not NOTSET: From ef1464c5a3a02d032242ffe10df5ef3f7e753d22 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 28 Mar 2024 13:17:55 -0400 Subject: [PATCH 0818/1178] Restore PyROS intro and disclaimer logging --- pyomo/contrib/pyros/pyros.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index c3335588b7b..582233c4a56 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -12,7 +12,6 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging from pyomo.common.config import document_kwargs_from_configdict -from pyomo.common.collections import Bunch from pyomo.core.base.block import Block from pyomo.core.expr import value from pyomo.core.base.var import Var @@ -20,7 +19,7 @@ from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.config import pyros_config +from pyomo.contrib.pyros.config import pyros_config, logger_domain from pyomo.contrib.pyros.util import ( recast_to_min_obj, add_decision_rule_constraints, @@ -347,6 +346,23 @@ def solve( global_solver=global_solver, ) ) + + # we want to log the intro and disclaimer in + # advance of assembling the config. + # this helps clarify to the user that any + # messages logged during assembly of the config + # were, in fact, logged after PyROS was initiated + progress_logger = logger_domain( + kwds.get( + "progress_logger", + kwds.get("options", dict()).get( + "progress_logger", default_pyros_solver_logger + ), + ) + ) + self._log_intro(logger=progress_logger, level=logging.INFO) + self._log_disclaimer(logger=progress_logger, level=logging.INFO) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) self._log_config( logger=config.progress_logger, From eb83c2e62485aefa0b0d92cd896b073f6a3ed010 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 11:24:55 -0600 Subject: [PATCH 0819/1178] Add test for option setting behavior --- pyomo/contrib/solver/tests/unit/test_base.py | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 179d9823679..ecf788b17d9 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -272,6 +272,41 @@ def test_map_config(self): with self.assertRaises(AttributeError): print(instance.config.keepfiles) + def test_solver_options_behavior(self): + # options can work in multiple ways (set from instantiation, set + # after instantiation, set during solve). + # Test case 1: Set at instantiation + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + self.assertEqual(solver.options, {'max_iter': 6}) + + # Test case 2: Set later + solver = base.LegacySolverWrapper() + solver.options = {'max_iter': 4, 'foo': 'bar'} + self.assertEqual(solver.options, {'max_iter': 4, 'foo': 'bar'}) + + # Test case 3: pass some options to the mapping (aka, 'solve' command) + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # Test case 4: Set at instantiation and override during 'solve' call + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 6}) + def test_map_results(self): # Unclear how to test this pass From d9ca9879032a9da21a41f0ece5bf623e4553398f Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 28 Mar 2024 13:32:37 -0400 Subject: [PATCH 0820/1178] Update PyROS solver logging docs example --- doc/OnlineDocs/contributed_packages/pyros.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 76a751dd994..9faa6d1365f 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -903,10 +903,10 @@ Observe that the log contains the following information: :linenos: ============================================================================== - PyROS: The Pyomo Robust Optimization Solver, v1.2.9. - Pyomo version: 6.7.0 + PyROS: The Pyomo Robust Optimization Solver, v1.2.11. + Pyomo version: 6.7.2 Commit hash: unknown - Invoked at UTC 2023-12-16T00:00:00.000000 + Invoked at UTC 2024-03-28T00:00:00.000000 Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), John D. Siirola (2), Chrysanthos E. Gounaris (1) From b3024f5f74f903cd170c896de751966483695d39 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 12:49:14 -0600 Subject: [PATCH 0821/1178] Make error message more clear --- pyomo/contrib/solver/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 7e93bacd54b..e5794a8088c 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -577,7 +577,10 @@ def available(self, exception_flag=True): """ ans = super().available() if exception_flag and not ans: - raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') + raise ApplicationError( + f'Solver "{self.name}" is not available. ' + f'The returned status is: {ans}.' + ) return bool(ans) def license_is_valid(self) -> bool: From 9eb3da0032f54851fd4743d1244ee88e7b3672c4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 13:32:01 -0600 Subject: [PATCH 0822/1178] Update options to allow both options and solver_options and writer_config --- pyomo/contrib/solver/base.py | 35 ++++++++++- pyomo/contrib/solver/tests/unit/test_base.py | 64 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index e5794a8088c..918f436a212 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,8 +349,15 @@ class LegacySolverWrapper: """ def __init__(self, **kwargs): - if 'options' in kwargs: + if 'options' in kwargs and 'solver_options' in kwargs: + raise ApplicationError( + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." + ) + elif 'options' in kwargs: self.options = kwargs.pop('options') + elif 'solver_options' in kwargs: + self.solver_options = kwargs.pop('solver_options') super().__init__(**kwargs) # @@ -376,6 +383,8 @@ def _map_config( keepfiles=NOTSET, solnfile=NOTSET, options=NOTSET, + solver_options=NOTSET, + writer_config=NOTSET, ): """Map between legacy and new interface configuration options""" self.config = self.config() @@ -395,12 +404,27 @@ def _map_config( self.config.report_timing = report_timing if hasattr(self, 'options'): self.config.solver_options.set_value(self.options) - if options is not NOTSET: + if hasattr(self, 'solver_options'): + self.config.solver_options.set_value(self.solver_options) + if (options is not NOTSET) and (solver_options is not NOTSET): + # There is no reason for a user to be trying to mix both old + # and new options. That is silly. So we will yell at them. + # Example that would raise an error: + # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) + raise ApplicationError( + "Both 'options' and 'solver_options' were declared " + "in the 'solve' call. Please use one or the other, " + "not both." + ) + elif options is not NOTSET: # This block is trying to mimic the existing logic in the legacy # interface that allows users to pass initialized options to # the solver object and override them in the solve call. self.config.solver_options.set_value(options) - + elif solver_options is not NOTSET: + self.config.solver_options.set_value(solver_options) + if writer_config is not NOTSET: + self.config.writer_config.set_value(writer_config) # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" if raise_exception_on_nonoptimal_result is not NOTSET: @@ -526,7 +550,10 @@ def solve( options: Optional[Dict] = None, keepfiles: bool = False, symbolic_solver_labels: bool = False, + # These are for forward-compatibility raise_exception_on_nonoptimal_result: bool = False, + solver_options: Optional[Dict] = None, + writer_config: Optional[Dict] = None, ): """ Solve method: maps new solve method style to backwards compatible version. @@ -552,6 +579,8 @@ def solve( 'keepfiles', 'solnfile', 'options', + 'solver_options', + 'writer_config', ) loc = locals() filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None} diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index ecf788b17d9..287116008ab 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -14,6 +14,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.contrib.solver import base +from pyomo.common.errors import ApplicationError class TestSolverBase(unittest.TestCase): @@ -307,6 +308,69 @@ def test_solver_options_behavior(self): self.assertEqual(solver.config.solver_options, {'max_iter': 4}) self.assertEqual(solver.options, {'max_iter': 6}) + # solver_options are also supported + # Test case 1: set at instantiation + solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + self.assertEqual(solver.solver_options, {'max_iter': 6}) + + # Test case 2: Set later + solver = base.LegacySolverWrapper() + solver.solver_options = {'max_iter': 4, 'foo': 'bar'} + self.assertEqual(solver.solver_options, {'max_iter': 4, 'foo': 'bar'}) + + # Test case 3: pass some solver_options to the mapping (aka, 'solve' command) + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # Test case 4: Set at instantiation and override during 'solve' call + solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + self.assertEqual(solver.solver_options, {'max_iter': 6}) + + # users can mix... sort of + # Test case 1: Initialize with options, solve with solver_options + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # users CANNOT initialize both values at the same time, because how + # do we know what to do with it then? + # Test case 1: Class instance + with self.assertRaises(ApplicationError): + solver = base.LegacySolverWrapper( + options={'max_iter': 6}, solver_options={'max_iter': 4} + ) + # Test case 2: Passing to `solve` + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + with self.assertRaises(ApplicationError): + solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) + def test_map_results(self): # Unclear how to test this pass From a96cd1074d9bcdcb555b98278226b034462df04f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 14:42:11 -0600 Subject: [PATCH 0823/1178] Add a helpful comment --- pyomo/contrib/solver/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 918f436a212..756babc6b20 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,6 +349,8 @@ class LegacySolverWrapper: """ def __init__(self, **kwargs): + # There is no reason for a user to be trying to mix both old + # and new options. That is silly. So we will yell at them. if 'options' in kwargs and 'solver_options' in kwargs: raise ApplicationError( "Both 'options' and 'solver_options' were requested. " From 5cd0e653c2386e3e4436769c1464f98645091c6a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 14:48:57 -0600 Subject: [PATCH 0824/1178] Accidentally removed solver_io check --- pyomo/contrib/solver/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 756babc6b20..064c411c74d 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,6 +349,8 @@ class LegacySolverWrapper: """ def __init__(self, **kwargs): + if 'solver_io' in kwargs: + raise NotImplementedError('Still working on this') # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. if 'options' in kwargs and 'solver_options' in kwargs: From 71709fd7bc6ed63ae82dca1ce05cbdcb1a4fcc36 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 15:04:05 -0600 Subject: [PATCH 0825/1178] Add information about options to docs --- .../developer_reference/solvers.rst | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 6168da3480e..94fb684236f 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -84,6 +84,37 @@ be used with other Pyomo tools / capabilities. ... 3 Declarations: x y obj +In keeping with our commitment to backwards compatibility, both the legacy and +future methods of specifying solver options are supported: + +.. testcode:: + :skipif: not ipopt_available + + import pyomo.environ as pyo + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + # Backwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, options={'max_iter' : 6}) + # Forwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, solver_options={'max_iter' : 6}) + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + Using the new interfaces directly ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From cd33b434126e77769462641f0cdc40cdfdab8211 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 17:03:03 -0600 Subject: [PATCH 0826/1178] Consolidate into just self.options --- pyomo/contrib/solver/base.py | 9 +++------ pyomo/contrib/solver/tests/unit/test_base.py | 13 ++++--------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 064c411c74d..79e677b6226 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -361,7 +361,7 @@ def __init__(self, **kwargs): elif 'options' in kwargs: self.options = kwargs.pop('options') elif 'solver_options' in kwargs: - self.solver_options = kwargs.pop('solver_options') + self.options = kwargs.pop('solver_options') super().__init__(**kwargs) # @@ -408,17 +408,14 @@ def _map_config( self.config.report_timing = report_timing if hasattr(self, 'options'): self.config.solver_options.set_value(self.options) - if hasattr(self, 'solver_options'): - self.config.solver_options.set_value(self.solver_options) if (options is not NOTSET) and (solver_options is not NOTSET): # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. # Example that would raise an error: # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) raise ApplicationError( - "Both 'options' and 'solver_options' were declared " - "in the 'solve' call. Please use one or the other, " - "not both." + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." ) elif options is not NOTSET: # This block is trying to mimic the existing logic in the legacy diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 287116008ab..fb8020bedf6 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -311,14 +311,9 @@ def test_solver_options_behavior(self): # solver_options are also supported # Test case 1: set at instantiation solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) - self.assertEqual(solver.solver_options, {'max_iter': 6}) - - # Test case 2: Set later - solver = base.LegacySolverWrapper() - solver.solver_options = {'max_iter': 4, 'foo': 'bar'} - self.assertEqual(solver.solver_options, {'max_iter': 4, 'foo': 'bar'}) + self.assertEqual(solver.options, {'max_iter': 6}) - # Test case 3: pass some solver_options to the mapping (aka, 'solve' command) + # Test case 2: pass some solver_options to the mapping (aka, 'solve' command) solver = base.LegacySolverWrapper() config = ConfigDict(implicit=True) config.declare( @@ -329,7 +324,7 @@ def test_solver_options_behavior(self): solver._map_config(solver_options={'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) - # Test case 4: Set at instantiation and override during 'solve' call + # Test case 3: Set at instantiation and override during 'solve' call solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) config = ConfigDict(implicit=True) config.declare( @@ -339,7 +334,7 @@ def test_solver_options_behavior(self): solver.config = config solver._map_config(solver_options={'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) - self.assertEqual(solver.solver_options, {'max_iter': 6}) + self.assertEqual(solver.options, {'max_iter': 6}) # users can mix... sort of # Test case 1: Initialize with options, solve with solver_options From 4d8e1c3d7c6229dcfe712d7f71e125bc74f127cc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 13:23:48 -0600 Subject: [PATCH 0827/1178] Move from isinstance to ctype for verifying component type --- .../piecewise_to_gdp_transformation.py | 4 +-- .../core/plugins/transform/add_slack_vars.py | 5 ++-- pyomo/core/plugins/transform/scaling.py | 6 ++--- pyomo/repn/plugins/nl_writer.py | 25 +++++++++++-------- pyomo/util/calc_var_value.py | 4 +-- pyomo/util/report_scaling.py | 4 +-- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 779bb601c71..5417cbc17f4 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -33,7 +33,7 @@ Any, ) from pyomo.core.base import Transformation -from pyomo.core.base.block import BlockData, Block +from pyomo.core.base.block import Block from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction from pyomo.gdp.util import is_child_of @@ -147,7 +147,7 @@ def _apply_to_impl(self, instance, **kwds): self._transform_piecewise_linear_function( t, config.descend_into_expressions ) - elif t.ctype is Block or isinstance(t, BlockData): + elif issubclass(t.ctype, Block): self._transform_block(t, config.descend_into_expressions) elif t.ctype is Constraint: if not config.descend_into_expressions: diff --git a/pyomo/core/plugins/transform/add_slack_vars.py b/pyomo/core/plugins/transform/add_slack_vars.py index 0007f8de7ad..39903384729 100644 --- a/pyomo/core/plugins/transform/add_slack_vars.py +++ b/pyomo/core/plugins/transform/add_slack_vars.py @@ -23,7 +23,6 @@ from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base import ComponentUID -from pyomo.core.base.constraint import ConstraintData from pyomo.common.deprecation import deprecation_warning @@ -42,7 +41,7 @@ def target_list(x): # [ESJ 07/15/2020] We have to just pass it through because we need the # instance in order to be able to do anything about it... return [x] - elif isinstance(x, (Constraint, ConstraintData)): + elif getattr(x, 'ctype', None) is Constraint: return [x] elif hasattr(x, '__iter__'): ans = [] @@ -53,7 +52,7 @@ def target_list(x): deprecation_msg = None # same as above... ans.append(i) - elif isinstance(i, (Constraint, ConstraintData)): + elif getattr(i, 'ctype', None) is Constraint: ans.append(i) else: raise ValueError( diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index ef418f094ae..e962352668c 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -15,8 +15,6 @@ Var, Constraint, Objective, - ConstraintData, - ObjectiveData, Suffix, value, ) @@ -197,7 +195,7 @@ def _apply_to(self, model, rename=True): already_scaled.add(id(c)) # perform the constraint/objective scaling and variable sub scaling_factor = component_scaling_factor_map[c] - if isinstance(c, ConstraintData): + if c.ctype is Constraint: body = scaling_factor * replace_expressions( expr=c.body, substitution_map=variable_substitution_dict, @@ -226,7 +224,7 @@ def _apply_to(self, model, rename=True): else: c.set_value((lower, body, upper)) - elif isinstance(c, ObjectiveData): + elif c.ctype is Objective: c.expr = scaling_factor * replace_expressions( expr=c.expr, substitution_map=variable_substitution_dict, diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8cc73b5e3fe..76599f74228 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -437,6 +437,7 @@ def store(self, obj, val): self.values[obj] = val def compile(self, column_order, row_order, obj_order, model_id): + var_con_obj = {Var, Constraint, Objective} missing_component_data = ComponentSet() unknown_data = ComponentSet() queue = [self.values.items()] @@ -462,18 +463,20 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (VarData, ConstraintData, ObjectiveData)): - missing_component_data.add(obj) - elif isinstance(obj, (Var, Constraint, Objective)): - # Expand this indexed component to store the - # individual ComponentDatas, but ONLY if the - # component data is not in the original dictionary - # of values that we extracted from the Suffixes - queue.append( - product( - filterfalse(self.values.__contains__, obj.values()), (val,) + elif getattr(obj, 'ctype', None) in var_con_obj: + if obj.is_indexed(): + # Expand this indexed component to store the + # individual ComponentDatas, but ONLY if the + # component data is not in the original dictionary + # of values that we extracted from the Suffixes + queue.append( + product( + filterfalse(self.values.__contains__, obj.values()), + (val,), + ) ) - ) + else: + missing_component_data.add(obj) else: unknown_data.add(obj) if missing_component_data: diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index 254b82c59cd..156ad56dffb 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -12,7 +12,7 @@ from pyomo.common.errors import IterationLimitError from pyomo.common.numeric_types import native_numeric_types, native_complex_types, value from pyomo.core.expr.calculus.derivatives import differentiate -from pyomo.core.base.constraint import Constraint, ConstraintData +from pyomo.core.base.constraint import Constraint import logging @@ -81,7 +81,7 @@ def calculate_variable_from_constraint( """ # Leverage all the Constraint logic to process the incoming tuple/expression - if not isinstance(constraint, ConstraintData): + if not getattr(constraint, 'ctype', None) is Constraint: constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 7619662c482..02b3710c334 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -13,7 +13,7 @@ import math from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import VarData +from pyomo.core.base.var import Var from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, VarData): + if getattr(_v, 'ctype', None) is Var: if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) From 812a1b7fd80be976c5cba7ff93c5bc68d9f795da Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 18:52:18 -0600 Subject: [PATCH 0828/1178] Remove giant status if tree --- pyomo/contrib/solver/gurobi_direct.py | 65 +++++++++++++-------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 7b5ec6ed904..f5f1bca7184 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -88,6 +88,7 @@ class GurobiDirect(SolverBase): _available = None _num_instances = 0 + _tc_map = None def __init__(self, **kwds): super().__init__(**kwds) @@ -256,7 +257,6 @@ def _postsolve(self, timer: HierarchicalTimer, loader): config = self._config gprob = loader._grb_model - grb = gurobipy.GRB status = gprob.Status results = Results() @@ -264,45 +264,16 @@ def _postsolve(self, timer: HierarchicalTimer, loader): results.timing_info.gurobi_time = gprob.Runtime if gprob.SolCount > 0: - if status == grb.OPTIMAL: + if status == gurobipy.GRB.OPTIMAL: results.solution_status = SolutionStatus.optimal else: results.solution_status = SolutionStatus.feasible else: results.solution_status = SolutionStatus.noSolution - if status == grb.LOADED: # problem is loaded, but no solution - results.termination_condition = TerminationCondition.unknown - elif status == grb.OPTIMAL: # optimal - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) - elif status == grb.INFEASIBLE: - results.termination_condition = TerminationCondition.provenInfeasible - elif status == grb.INF_OR_UNBD: - results.termination_condition = TerminationCondition.infeasibleOrUnbounded - elif status == grb.UNBOUNDED: - results.termination_condition = TerminationCondition.unbounded - elif status == grb.CUTOFF: - results.termination_condition = TerminationCondition.objectiveLimit - elif status == grb.ITERATION_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit - elif status == grb.NODE_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit - elif status == grb.TIME_LIMIT: - results.termination_condition = TerminationCondition.maxTimeLimit - elif status == grb.SOLUTION_LIMIT: - results.termination_condition = TerminationCondition.unknown - elif status == grb.INTERRUPTED: - results.termination_condition = TerminationCondition.interrupted - elif status == grb.NUMERIC: - results.termination_condition = TerminationCondition.unknown - elif status == grb.SUBOPTIMAL: - results.termination_condition = TerminationCondition.unknown - elif status == grb.USER_OBJ_LIMIT: - results.termination_condition = TerminationCondition.objectiveLimit - else: - results.termination_condition = TerminationCondition.unknown + results.termination_condition = self._get_tc_map().get( + status, TerminationCondition.unknown + ) if ( results.termination_condition @@ -310,7 +281,9 @@ def _postsolve(self, timer: HierarchicalTimer, loader): and config.raise_exception_on_nonoptimal_result ): raise RuntimeError( - 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + 'Solver did not find the optimal solution. Set ' + 'opt.config.raise_exception_on_nonoptimal_result=False ' + 'to bypass this error.' ) results.incumbent_objective = None @@ -348,3 +321,25 @@ def _postsolve(self, timer: HierarchicalTimer, loader): timer.stop('load solution') return results + + def _get_tc_map(self): + if GurobiDirect._tc_map is None: + grb = gurobipy.GRB + tc = TerminationCondition + GurobiDirect._tc_map = { + grb.LOADED: tc.unknown, # problem is loaded, but no solution + grb.OPTIMAL: tc.convergenceCriteriaSatisfied, + grb.INFEASIBLE: tc.provenInfeasible, + grb.INF_OR_UNBD: tc.infeasibleOrUnbounded, + grb.UNBOUNDED: tc.unbounded, + grb.CUTOFF: tc.objectiveLimit, + grb.ITERATION_LIMIT: tc.iterationLimit, + grb.NODE_LIMIT: tc.iterationLimit, + grb.TIME_LIMIT: tc.maxTimeLimit, + grb.SOLUTION_LIMIT: tc.unknown, + grb.INTERRUPTED: tc.interrupted, + grb.NUMERIC: tc.unknown, + grb.SUBOPTIMAL: tc.unknown, + grb.USER_OBJ_LIMIT: tc.objectiveLimit, + } + return GurobiDirect._tc_map From 9d4b9131c76b337a41ff12cf4ea6f55062ae3115 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 19:41:34 -0600 Subject: [PATCH 0829/1178] NFC: apply black --- pyomo/core/plugins/transform/scaling.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index e962352668c..11d4ac8c493 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -10,14 +10,7 @@ # ___________________________________________________________________________ from pyomo.common.collections import ComponentMap -from pyomo.core.base import ( - Block, - Var, - Constraint, - Objective, - Suffix, - value, -) +from pyomo.core.base import Block, Var, Constraint, Objective, Suffix, value from pyomo.core.plugins.transform.hierarchy import Transformation from pyomo.core.base import TransformationFactory from pyomo.core.base.suffix import SuffixFinder From 711fafe517ce8f4d21a89c462e9083a40dfb55ca Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 19:42:37 -0600 Subject: [PATCH 0830/1178] NFC: apply black --- pyomo/contrib/solver/gurobi_direct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index f5f1bca7184..1164686f0f1 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -328,7 +328,7 @@ def _get_tc_map(self): tc = TerminationCondition GurobiDirect._tc_map = { grb.LOADED: tc.unknown, # problem is loaded, but no solution - grb.OPTIMAL: tc.convergenceCriteriaSatisfied, + grb.OPTIMAL: tc.convergenceCriteriaSatisfied, grb.INFEASIBLE: tc.provenInfeasible, grb.INF_OR_UNBD: tc.infeasibleOrUnbounded, grb.UNBOUNDED: tc.unbounded, From 8c4fb774a30a621e7890dd006cc9e83931c545e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 20:17:52 -0600 Subject: [PATCH 0831/1178] Remove debugging; expand comment explaining difference in cut-and-paste tests --- pyomo/repn/tests/test_standard_form.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index 591703e6ae8..4c66ae87c41 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -256,11 +256,11 @@ def test_alternative_forms(self): [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] ) self.assertTrue(np.all(repn.A == ref)) - print(repn) - print(repn.b) self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) - # Note that the solution is a mix of inequality and equality constraints + # Note that the mixed_form solution is a mix of inequality and + # equality constraints, so we cannot (easily) reuse the + # _verify_solutions helper (as in the above cases): # self._verify_solution(soln, repn, False) repn = LinearStandardFormCompiler().write( From 7d9d490e05a2dca6a4b97f731c383295c8b40970 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:04:36 -0600 Subject: [PATCH 0832/1178] Change check for options --- pyomo/contrib/solver/base.py | 19 +++++++++---------- pyomo/contrib/solver/tests/unit/test_base.py | 5 ++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 79e677b6226..9c19fccaa89 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -353,14 +353,13 @@ def __init__(self, **kwargs): raise NotImplementedError('Still working on this') # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. - if 'options' in kwargs and 'solver_options' in kwargs: - raise ApplicationError( - "Both 'options' and 'solver_options' were requested. " - "Please use one or the other, not both." - ) - elif 'options' in kwargs: - self.options = kwargs.pop('options') - elif 'solver_options' in kwargs: + self.options = kwargs.pop('options', None) + if 'solver_options' in kwargs: + if self.options is not None: + raise ValueError( + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." + ) self.options = kwargs.pop('solver_options') super().__init__(**kwargs) @@ -406,14 +405,14 @@ def _map_config( self.config.time_limit = timelimit if report_timing is not NOTSET: self.config.report_timing = report_timing - if hasattr(self, 'options'): + if self.options is not None: self.config.solver_options.set_value(self.options) if (options is not NOTSET) and (solver_options is not NOTSET): # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. # Example that would raise an error: # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) - raise ApplicationError( + raise ValueError( "Both 'options' and 'solver_options' were requested. " "Please use one or the other, not both." ) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index fb8020bedf6..b52f96ba903 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -14,7 +14,6 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.contrib.solver import base -from pyomo.common.errors import ApplicationError class TestSolverBase(unittest.TestCase): @@ -351,7 +350,7 @@ def test_solver_options_behavior(self): # users CANNOT initialize both values at the same time, because how # do we know what to do with it then? # Test case 1: Class instance - with self.assertRaises(ApplicationError): + with self.assertRaises(ValueError): solver = base.LegacySolverWrapper( options={'max_iter': 6}, solver_options={'max_iter': 4} ) @@ -363,7 +362,7 @@ def test_solver_options_behavior(self): ConfigDict(implicit=True, description="Options to pass to the solver."), ) solver.config = config - with self.assertRaises(ApplicationError): + with self.assertRaises(ValueError): solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) def test_map_results(self): From bfb9beb7488e722aa6b336b8a4c89ce39561a81c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:28:51 -0600 Subject: [PATCH 0833/1178] March 2024: Typos Update --- .github/workflows/typos.toml | 24 ++++++++++++++++++++++++ pyomo/contrib/latex_printer/__init__.py | 13 +------------ pyomo/core/base/component.py | 4 ++-- pyomo/core/base/indexed_component.py | 2 +- pyomo/core/base/reference.py | 2 +- pyomo/gdp/plugins/fix_disjuncts.py | 2 +- pyomo/gdp/tests/models.py | 2 +- pyomo/network/foqus_graph.py | 4 ++-- pyomo/solvers/plugins/solvers/GAMS.py | 20 ++++++++++---------- 9 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 23f94fc8afd..f98a6122ffd 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,4 +40,28 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" +# Ignore the shorthand ans for and +ans = "ans" +# Ignore the keyword arange +arange = "arange" +# Ignore IIS +IIS = "IIS" +iis = "iis" +# Ignore PN +PN = "PN" +# Ignore hd +hd = "hd" +# Ignore opf +opf = "opf" +# Ignore FRE +FRE = "FRE" +# Ignore MCH +MCH = "MCH" +# Ignore RO +ro = "ro" +RO = "RO" +# Ignore EOF - end of file +EOF = "EOF" +# Ignore lst as shorthand for list +lst = "lst" # AS NEEDED: Add More Words Below diff --git a/pyomo/contrib/latex_printer/__init__.py b/pyomo/contrib/latex_printer/__init__.py index c434b53dfe1..02eaa636a36 100644 --- a/pyomo/contrib/latex_printer/__init__.py +++ b/pyomo/contrib/latex_printer/__init__.py @@ -9,22 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - # Recommended just to build all of the appropriate things import pyomo.environ # Remove one layer of .latex_printer -# import statemnt is now: +# import statement is now: # from pyomo.contrib.latex_printer import latex_printer try: from pyomo.contrib.latex_printer.latex_printer import latex_printer diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 22c2bc4b804..9b1929daa06 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -368,7 +368,7 @@ def pprint(self, ostream=None, verbose=False, prefix=""): @property def name(self): - """Get the fully qualifed component name.""" + """Get the fully qualified component name.""" return self.getname(fully_qualified=True) # Adding a setter here to help users adapt to the new @@ -664,7 +664,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): @property def name(self): - """Get the fully qualifed component name.""" + """Get the fully qualified component name.""" return self.getname(fully_qualified=True) # Allow setting a component's name if it is not owned by a parent diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index e1be613d666..37a62e5c4d7 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -731,7 +731,7 @@ def __delitem__(self, index): # this supports "del m.x[:,1]" through a simple recursive call if index.__class__ is IndexedComponent_slice: - # Assert that this slice ws just generated + # Assert that this slice was just generated assert len(index._call_stack) == 1 # Make a copy of the slicer items *before* we start # iterating over it (since we will be removing items!). diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 2279db067a6..84cccec9749 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -579,7 +579,7 @@ def Reference(reference, ctype=NOTSET): :py:class:`IndexedComponent`. If the indices associated with wildcards in the component slice all - refer to the same :py:class:`Set` objects for all data identifed by + refer to the same :py:class:`Set` objects for all data identified by the slice, then the resulting indexed component will be indexed by the product of those sets. However, if all data do not share common set objects, or only a subset of indices in a multidimentional set diff --git a/pyomo/gdp/plugins/fix_disjuncts.py b/pyomo/gdp/plugins/fix_disjuncts.py index 44a9d91d513..172363caab7 100644 --- a/pyomo/gdp/plugins/fix_disjuncts.py +++ b/pyomo/gdp/plugins/fix_disjuncts.py @@ -52,7 +52,7 @@ class GDP_Disjunct_Fixer(Transformation): This reclassifies all disjuncts in the passed model instance as ctype Block and deactivates the constraints and disjunctions within inactive disjuncts. - In addition, it transforms relvant LogicalConstraints and BooleanVars so + In addition, it transforms relevant LogicalConstraints and BooleanVars so that the resulting model is a (MI)(N)LP (where it is only mixed-integer if the model contains integer-domain Vars or BooleanVars which were not indicator_vars of Disjuncs. diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index 0b84641899c..2995cacb450 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -840,7 +840,7 @@ def makeAnyIndexedDisjunctionOfDisjunctDatas(): build from DisjunctDatas. Identical mathematically to makeDisjunctionOfDisjunctDatas. - Used to test that the right things happen for a case where soemone + Used to test that the right things happen for a case where someone implements an algorithm which iteratively generates disjuncts and retransforms""" m = ConcreteModel() diff --git a/pyomo/network/foqus_graph.py b/pyomo/network/foqus_graph.py index e4cf3b92014..d904fa54008 100644 --- a/pyomo/network/foqus_graph.py +++ b/pyomo/network/foqus_graph.py @@ -358,9 +358,9 @@ def scc_calculation_order(self, sccNodes, ie, oe): done = False for i in range(len(sccNodes)): for j in range(len(sccNodes)): - for ine in ie[i]: + for in_e in ie[i]: for oute in oe[j]: - if ine == oute: + if in_e == oute: adj[j].append(i) adjR[i].append(j) done = True diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index be3499a2f6b..606098e5b7b 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -198,8 +198,8 @@ def _get_version(self): return _extract_version('') from gams import GamsWorkspace - ws = GamsWorkspace() - version = tuple(int(i) for i in ws._version.split('.')[:4]) + workspace = GamsWorkspace() + version = tuple(int(i) for i in workspace._version.split('.')[:4]) while len(version) < 4: version += (0,) return version @@ -209,8 +209,8 @@ def _run_simple_model(self, n): try: from gams import GamsWorkspace, DebugLevel - ws = GamsWorkspace(debug=DebugLevel.Off, working_directory=tmpdir) - t1 = ws.add_job_from_string(self._simple_model(n)) + workspace = GamsWorkspace(debug=DebugLevel.Off, working_directory=tmpdir) + t1 = workspace.add_job_from_string(self._simple_model(n)) t1.run() return True except: @@ -330,12 +330,12 @@ def solve(self, *args, **kwds): if tmpdir is not None and os.path.exists(tmpdir): newdir = False - ws = GamsWorkspace( + workspace = GamsWorkspace( debug=DebugLevel.KeepFiles if keepfiles else DebugLevel.Off, working_directory=tmpdir, ) - t1 = ws.add_job_from_string(output_file.getvalue()) + t1 = workspace.add_job_from_string(output_file.getvalue()) try: with OutputStream(tee=tee, logfile=logfile) as output_stream: @@ -349,7 +349,7 @@ def solve(self, *args, **kwds): # Always name working directory or delete files, # regardless of any errors. if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -359,7 +359,7 @@ def solve(self, *args, **kwds): except: # Catch other errors and remove files first if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -398,7 +398,7 @@ def solve(self, *args, **kwds): extract_rc = 'rc' in model_suffixes results = SolverResults() - results.problem.name = os.path.join(ws.working_directory, t1.name + '.gms') + results.problem.name = os.path.join(workspace.working_directory, t1.name + '.gms') results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = t1.out_db["NUMVAR"].find_record().value @@ -587,7 +587,7 @@ def solve(self, *args, **kwds): results.solution.insert(soln) if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted From ee92b7001c599a373c520be75097fdac05cfdd68 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:30:52 -0600 Subject: [PATCH 0834/1178] Comment is misleading --- .github/workflows/typos.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index f98a6122ffd..4d69cde34e1 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,7 +40,7 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" -# Ignore the shorthand ans for and +# Ignore the shorthand ans for answer ans = "ans" # Ignore the keyword arange arange = "arange" From a69c1b8615fd58db538de28186f54fcd6b594cb9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:35:51 -0600 Subject: [PATCH 0835/1178] Address @jsiirola 's comments and apply black --- pyomo/network/foqus_graph.py | 4 ++-- pyomo/solvers/plugins/solvers/GAMS.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/network/foqus_graph.py b/pyomo/network/foqus_graph.py index d904fa54008..7c6c05256d9 100644 --- a/pyomo/network/foqus_graph.py +++ b/pyomo/network/foqus_graph.py @@ -359,8 +359,8 @@ def scc_calculation_order(self, sccNodes, ie, oe): for i in range(len(sccNodes)): for j in range(len(sccNodes)): for in_e in ie[i]: - for oute in oe[j]: - if in_e == oute: + for out_e in oe[j]: + if in_e == out_e: adj[j].append(i) adjR[i].append(j) done = True diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index 606098e5b7b..c0bab4dc23e 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -349,7 +349,9 @@ def solve(self, *args, **kwds): # Always name working directory or delete files, # regardless of any errors. if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) + print( + "\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory + ) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -398,7 +400,9 @@ def solve(self, *args, **kwds): extract_rc = 'rc' in model_suffixes results = SolverResults() - results.problem.name = os.path.join(workspace.working_directory, t1.name + '.gms') + results.problem.name = os.path.join( + workspace.working_directory, t1.name + '.gms' + ) results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = t1.out_db["NUMVAR"].find_record().value From 6e182bed965e98ead3578c1a2d79244c93760e00 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 2 Apr 2024 11:29:38 +0200 Subject: [PATCH 0836/1178] Add MAiNGO to test_perisistent_solvers.py --- .../solvers/tests/test_persistent_solvers.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index ae189aca701..c063adc2bfe 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,7 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -36,11 +36,23 @@ ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs), + ('maingo', MAiNGO), ] -mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] -nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] -miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] +mip_solvers = [ + ('gurobi', Gurobi), + ('cplex', Cplex), + ('cbc', Cbc), + ('highs', Highs), + ('maingo', MAiNGO), +] +nlp_solvers = [('ipopt', Ipopt), ('maingo', MAiNGO)] +qcp_solvers = [ + ('gurobi', Gurobi), + ('ipopt', Ipopt), + ('cplex', Cplex), + ('maingo', MAiNGO), +] +miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('maingo', MAiNGO)] only_child_vars_options = [True, False] From fc4bf437bc6ba5919bebed55ef24f95a77a80aa2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:26:48 -0600 Subject: [PATCH 0837/1178] Skip test under CyIpopt 1.4.0 --- .../contrib/pynumero/examples/tests/test_cyipopt_examples.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 408a0197382..bcd3b5d8bf5 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -266,6 +266,11 @@ def test_cyipopt_functor(self): s = df['ca_bal'] self.assertAlmostEqual(s.iloc[6], 0, places=3) + @unittest.skipIf( + cyipopt_core.__version__ == "1.4.0", + "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " + "(see mechmotum/cyipopt#249", + ) def test_cyipopt_callback_halt(self): ex = import_file( os.path.join(example_dir, 'callback', 'cyipopt_callback_halt.py') From 38d49d43293c45f50ccfbc9fbe7e3d57ed638a03 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:27:20 -0600 Subject: [PATCH 0838/1178] Fix bug in retrieving cyipopt version --- .../contrib/pynumero/algorithms/solvers/cyipopt_solver.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index cdea542295b..53616298415 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -319,7 +319,13 @@ def license_is_valid(self): return True def version(self): - return tuple(int(_) for _ in cyipopt.__version__.split(".")) + def _int(x): + try: + return int(x) + except: + return x + + return tuple(_int(_) for _ in cyipopt_interface.cyipopt.__version__.split(".")) def solve(self, model, **kwds): config = self.config(kwds, preserve_implicit=True) From c74427aeee68050d21c7d80929f9a1d440253c26 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:27:43 -0600 Subject: [PATCH 0839/1178] Fix import from a deprecated location --- .../contrib/pynumero/examples/tests/test_cyipopt_examples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index bcd3b5d8bf5..55dccd6a0ed 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -44,11 +44,13 @@ raise unittest.SkipTest("Pynumero needs the ASL extension to run CyIpopt tests") import pyomo.contrib.pynumero.algorithms.solvers.cyipopt_solver as cyipopt_solver +from pyomo.contrib.pynumero.interfaces.cyipopt_interface import cyipopt_available -if not cyipopt_solver.cyipopt_available: +if not cyipopt_available: raise unittest.SkipTest("PyNumero needs CyIpopt installed to run CyIpopt tests") import cyipopt as cyipopt_core + example_dir = os.path.join(this_file_dir(), '..') From 55c78059667d5412df28cab3dd0905e66aa197da Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:35:02 -0600 Subject: [PATCH 0840/1178] NFC: fix a message typo --- pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 55dccd6a0ed..a0e17df918a 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -271,7 +271,7 @@ def test_cyipopt_functor(self): @unittest.skipIf( cyipopt_core.__version__ == "1.4.0", "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " - "(see mechmotum/cyipopt#249", + "(see mechmotum/cyipopt#249)", ) def test_cyipopt_callback_halt(self): ex = import_file( From 50e2316be4d928694efb53fb11ea94546ee24e9f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:38:37 -0600 Subject: [PATCH 0841/1178] Use our version() method to test cyipopt version --- pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index a0e17df918a..2df43c1e797 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -269,7 +269,7 @@ def test_cyipopt_functor(self): self.assertAlmostEqual(s.iloc[6], 0, places=3) @unittest.skipIf( - cyipopt_core.__version__ == "1.4.0", + cyipopt_solver.PyomoCyIpoptSolver().version() == (1, 4, 0), "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " "(see mechmotum/cyipopt#249)", ) From 643865c5f1828c581609f8920d3c0fe8b0c4da41 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 11:27:10 -0600 Subject: [PATCH 0842/1178] Change to NaNs except for objectives --- pyomo/contrib/solver/base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 9c19fccaa89..6359b49d945 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -465,13 +465,14 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - legacy_results.problem.number_of_constraints = len( - list(model.component_map(ctype=Constraint)) - ) - legacy_results.problem.number_of_variables = len( - list(model.component_map(ctype=Var)) + legacy_results.problem.number_of_constraints = float('nan') + legacy_results.problem.number_of_variables = float('nan') + number_of_objectives = sum( + 1 + for _ in model.component_data_objects( + Objective, active=True, descend_into=True + ) ) - number_of_objectives = len(list(model.component_map(ctype=Objective))) legacy_results.problem.number_of_objectives = number_of_objectives if number_of_objectives == 1: obj = get_objective(model) From 1456e56857f4b45fbab945034f6916fff416799a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 11:31:37 -0600 Subject: [PATCH 0843/1178] Remove extra solutions logic, per commit to @andrewlee94's IDAES branch --- pyomo/contrib/solver/base.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 6359b49d945..cec392271f6 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -499,15 +499,7 @@ def _solution_handler( """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') - try: - model.solutions.add_symbol_map(symbol_map) - except AttributeError: - # Something wacky happens in IDAES due to the usage of ScalarBlock - # instead of PyomoModel. This is an attempt to fix that. - from pyomo.core.base.PyomoModel import ModelSolutions - - setattr(model, 'solutions', ModelSolutions(model)) - model.solutions.add_symbol_map(symbol_map) + model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True if load_solutions: From f2fd07c893fe5d13a6926407324fe4594a799f75 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 13:49:44 -0600 Subject: [PATCH 0844/1178] Skip Tests on Draft and WIP Pull Requests --- .github/workflows/test_pr_and_main.yml | 9 ++++++++- doc/OnlineDocs/contribution_guide.rst | 13 ++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..72366eb1353 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -7,6 +7,11 @@ on: pull_request: branches: - main + types: + - opened + - reopened + - synchronize + - ready_for_review workflow_dispatch: inputs: git-ref: @@ -34,6 +39,8 @@ jobs: lint: name: lint/style-and-typos runs-on: ubuntu-latest + if: | + contains(github.event.pull_request.title, '[WIP]') != true && !github.event.pull_request.draft steps: - name: Checkout Pyomo source uses: actions/checkout@v4 @@ -733,7 +740,7 @@ jobs: cover: name: process-coverage-${{ matrix.TARGET }} needs: build - if: always() # run even if a build job fails + if: success() || failure() # run even if a build job fails, but not if cancelled runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 10670627546..285bb656406 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -71,6 +71,10 @@ at least 70% coverage of the lines modified in the PR and prefer coverage closer to 90%. We also require that all tests pass before a PR will be merged. +.. note:: + If you are having issues getting tests to pass on your Pull Request, + please tag any of the core developers to ask for help. + The Pyomo main branch provides a Github Actions workflow (configured in the ``.github/`` directory) that will test any changes pushed to a branch with a subset of the complete test harness that includes @@ -82,13 +86,16 @@ This will enable the tests to run automatically with each push to your fork. At any point in the development cycle, a "work in progress" pull request may be opened by including '[WIP]' at the beginning of the PR -title. This allows your code changes to be tested by the full suite of -Pyomo's automatic -testing infrastructure. Any pull requests marked '[WIP]' will not be +title. Any pull requests marked '[WIP]' or draft will not be reviewed or merged by the core development team. However, any '[WIP]' pull request left open for an extended period of time without active development may be marked 'stale' and closed. +.. note:: + Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to + keep our backlog as available as possible. Please liberally use the provided + branch testing for draft functionality. + Python Version Support ++++++++++++++++++++++ From b082419502a7c024990c28a54d91bb3069bea011 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 13:57:37 -0600 Subject: [PATCH 0845/1178] Change language for branch test request --- doc/OnlineDocs/contribution_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 285bb656406..6054a8d2ba9 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -93,8 +93,8 @@ active development may be marked 'stale' and closed. .. note:: Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to - keep our backlog as available as possible. Please liberally use the provided - branch testing for draft functionality. + keep our backlog as available as possible. Please make use of the provided + branch test suite for evaluating / testing draft functionality. Python Version Support ++++++++++++++++++++++ From a3d6a430f21cd624aebf4559a7d60c8b5df12663 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 15:29:18 -0600 Subject: [PATCH 0846/1178] Removing unused imports --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 3ac532116aa..873bafabc76 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -22,8 +22,6 @@ from pyomo.common.collections import Bunch from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR -from pyomo.contrib.appsi.base import Solver -from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.gdpopt.create_oa_subproblems import ( add_util_block, add_disjunct_list, From 009f0d9af07c726977fa401502bc26fca52249c7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 15:37:44 -0600 Subject: [PATCH 0847/1178] Replace ProblemSense and the constants in kernel.objective with ObjectiveSense enum --- pyomo/common/enums.py | 38 ++++++++++++++++++++++++++++++++++ pyomo/core/kernel/objective.py | 5 +---- pyomo/opt/results/problem.py | 38 +++++++++++++++++++++++----------- 3 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 pyomo/common/enums.py diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py new file mode 100644 index 00000000000..c685843b41c --- /dev/null +++ b/pyomo/common/enums.py @@ -0,0 +1,38 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum + +from pyomo.common.deprecation import RenamedClass + +class ObjectiveSense(enum.IntEnum): + """Flag indicating if an objective is minimizing (1) or maximizing (-1). + + While the numeric values are arbitrary, there are parts of Pyomo + that rely on this particular choice of value. These values are also + consistent with some solvers (notably Gurobi). + + """ + minimize = 1 + maximize = -1 + + # Overloading __str__ is needed to match the behavior of the old + # pyutilib.enum class (removed June 2020). There are spots in the + # code base that expect the string representation for items in the + # enum to not include the class name. New uses of enum shouldn't + # need to do this. + def __str__(self): + return self.name + +minimize = ObjectiveSense.minimize +maximize = ObjectiveSense.maximize + + diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index 9aa8e3315ef..840c7cfd7e0 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -9,15 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.enums import minimize, maximize from pyomo.core.expr.numvalue import as_numeric from pyomo.core.kernel.base import _abstract_readwrite_property from pyomo.core.kernel.container_utils import define_simple_containers from pyomo.core.kernel.expression import IExpression -# Constants used to define the optimization sense -minimize = 1 -maximize = -1 - class IObjective(IExpression): """ diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 98f749f3aeb..e35bb155355 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -12,19 +12,33 @@ import enum from pyomo.opt.results.container import MapContainer +from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.common.enums import ObjectiveSense -class ProblemSense(str, enum.Enum): - unknown = 'unknown' - minimize = 'minimize' - maximize = 'maximize' - # Overloading __str__ is needed to match the behavior of the old - # pyutilib.enum class (removed June 2020). There are spots in the - # code base that expect the string representation for items in the - # enum to not include the class name. New uses of enum shouldn't - # need to do this. - def __str__(self): - return self.value +class ProblemSenseType(type): + @deprecated( + "pyomo.opt.results.problem.ProblemSense has been replaced by " + "pyomo.common.enums.ObjectiveSense", + version="6.7.2.dev0", + ) + def __getattr__(cls, attr): + if attr == 'minimize': + return ObjectiveSense.minimize + if attr == 'maximize': + return ObjectiveSense.maximize + if attr == 'unknown': + deprecation_warning( + "ProblemSense.unknown is no longer an allowable option. " + "Mapping 'unknown' to 'minimize'", + version="6.7.2.dev0", + ) + return ObjectiveSense.minimize + raise AttributeError(attr) + + +class ProblemSense(metaclass=ProblemSenseType): + pass class ProblemInformation(MapContainer): @@ -40,4 +54,4 @@ def __init__(self): self.declare('number_of_integer_variables') self.declare('number_of_continuous_variables') self.declare('number_of_nonzeros') - self.declare('sense', value=ProblemSense.unknown, required=True) + self.declare('sense', value=ProblemSense.minimize, required=True) From 75f577fb65f1eb5febb1e18a716ad02816194fc5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:34:21 -0600 Subject: [PATCH 0848/1178] Removing a base class I didn't use --- .../cp/scheduling_expr/sequence_expressions.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index d88504ac7e4..865a914b847 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -11,15 +11,8 @@ from pyomo.core.expr.logical_expr import BooleanExpression -# ESJ TODO: The naming in this file needs more thought, and it appears I do not -# need the base class. - -class SequenceVarExpression(BooleanExpression): - pass - - -class NoOverlapExpression(SequenceVarExpression): +class NoOverlapExpression(BooleanExpression): """ Expression representing that none of the IntervalVars in a SequenceVar overlap (if they are scheduled) @@ -35,7 +28,7 @@ def _to_string(self, values, verbose, smap): return "no_overlap(%s)" % values[0] -class FirstInSequenceExpression(SequenceVarExpression): +class FirstInSequenceExpression(BooleanExpression): """ Expression representing that the specified IntervalVar is the first in the sequence specified by SequenceVar (if it is scheduled) @@ -52,7 +45,7 @@ def _to_string(self, values, verbose, smap): return "first_in(%s, %s)" % (values[0], values[1]) -class LastInSequenceExpression(SequenceVarExpression): +class LastInSequenceExpression(BooleanExpression): """ Expression representing that the specified IntervalVar is the last in the sequence specified by SequenceVar (if it is scheduled) @@ -69,7 +62,7 @@ def _to_string(self, values, verbose, smap): return "last_in(%s, %s)" % (values[0], values[1]) -class BeforeInSequenceExpression(SequenceVarExpression): +class BeforeInSequenceExpression(BooleanExpression): """ Expression representing that one IntervalVar occurs before another in the sequence specified by the given SequenceVar (if both are scheduled) @@ -86,7 +79,7 @@ def _to_string(self, values, verbose, smap): return "before_in(%s, %s, %s)" % (values[0], values[1], values[2]) -class PredecessorToExpression(SequenceVarExpression): +class PredecessorToExpression(BooleanExpression): """ Expression representing that one IntervalVar is a direct predecessor to another in the sequence specified by the given SequenceVar (if both are scheduled) From 5bf1000b3b3be4e93da98f167d0921e3df9c7ae9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:37:28 -0600 Subject: [PATCH 0849/1178] Correcting copyright year --- pyomo/contrib/cp/debugging.py | 2 +- pyomo/contrib/cp/scheduling_expr/scheduling_logic.py | 2 +- pyomo/contrib/cp/scheduling_expr/sequence_expressions.py | 2 +- pyomo/contrib/cp/sequence_var.py | 2 +- pyomo/contrib/cp/tests/test_sequence_expressions.py | 2 +- pyomo/contrib/cp/tests/test_sequence_var.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/debugging.py b/pyomo/contrib/cp/debugging.py index 41c4d208de6..34fb105a571 100644 --- a/pyomo/contrib/cp/debugging.py +++ b/pyomo/contrib/cp/debugging.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index fc9cefebf4d..b28d536b594 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index 865a914b847..3ba799074de 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index b242b362f9d..486776f58da 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 218a4c0e1a0..62c868abfaf 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 404e21ca39c..ebff465a376 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 1090b6447ff831f2b07dac332f5634ec75888cce Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:41:18 -0600 Subject: [PATCH 0850/1178] Removing a stupid test --- pyomo/contrib/cp/tests/test_docplex_walker.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 2d08f6881dc..59d4f685c00 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -473,25 +473,6 @@ def test_all_diff_expression(self): self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) - def test_Boolean_args_in_all_diff_expression(self): - m = self.get_model() - m.a.domain = Integers - m.a.bounds = (11, 20) - m.c = LogicalConstraint(expr=all_different(m.a[1] == 13, m.b)) - - visitor = self.get_visitor() - expr = visitor.walk_expression((m.c.body, m.c, 0)) - - self.assertIn(id(m.a[1]), visitor.var_map) - a0 = visitor.var_map[id(m.a[1])] - self.assertIn(id(m.b), visitor.var_map) - b = visitor.var_map[id(m.b)] - - self.assertTrue(expr[1].equals(cp.all_diff(a0 == 13, b))) - - self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a0) - self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) - def test_count_if_expression(self): m = self.get_model() m.a.domain = Integers From 2899c82ee2c43a0407cee8839da1f98341697f77 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 22:14:48 -0600 Subject: [PATCH 0851/1178] Removing handling for IndexedSequenceVars because they can't appear in expressions right now --- pyomo/contrib/cp/repn/docplex_writer.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 439530eaf04..93b9974434a 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -551,18 +551,6 @@ def _before_sequence_var(visitor, child): return False, (_GENERAL, visitor.var_map[_id]) -def _before_indexed_sequence_var(visitor, child): - # ESJ TODO: I'm not sure we can encounter an indexed sequence var in an - # expression right now? - cpx_vars = {} - for i, v in child.items(): - cpx_sequence_var = _get_docplex_sequence_var(visitor, v) - visitor.var_map[id(v)] = cpx_sequence_var - visitor.pyomo_to_docplex[v] = cpx_sequence_var - cpx_vars[i] = cpx_sequence_var - return False, (_GENERAL, cpx_vars) - - def _before_interval_var(visitor, child): _id = id(child) if _id not in visitor.var_map: @@ -1063,7 +1051,6 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IndexedIntervalVar: _before_indexed_interval_var, ScalarSequenceVar: _before_sequence_var, _SequenceVarData: _before_sequence_var, - IndexedSequenceVar: _before_indexed_sequence_var, ScalarVar: _before_var, _GeneralVarData: _before_var, IndexedVar: _before_indexed_var, From f820efd887cf30884f22fd2403e9ef49f0b43b16 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 22:15:02 -0600 Subject: [PATCH 0852/1178] Adding some monomial expression tests --- pyomo/contrib/cp/tests/test_docplex_walker.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 59d4f685c00..4ad07c10ee7 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -237,6 +237,35 @@ def test_expression_with_mutable_param(self): self.assertTrue(expr[1].equals(4 * x)) + def test_monomial_expressions(self): + m = ConcreteModel() + m.x = Var(domain=Integers, bounds=(1, 4)) + m.p = Param(initialize=4, mutable=True) + + visitor = self.get_visitor() + + const_expr = 3 * m.x + nested_expr = (1 / m.p) * m.x + pow_expr = (m.p ** (0.5)) * m.x + + e = m.x * 4 + expr = visitor.walk_expression((e, e, 0)) + self.assertIn(id(m.x), visitor.var_map) + x = visitor.var_map[id(m.x)] + self.assertTrue(expr[1].equals(4 * x)) + + e = 1.0 * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(x)) + + e = (1 / m.p) * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(0.25 * x)) + + e = (m.p ** (0.5)) * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(2 * x)) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_LogicalExpressions(CommonTest): From ad69e15e9708279e757bbf53cb4e952886acf6b8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 22:15:24 -0600 Subject: [PATCH 0853/1178] Adding a test for my debugging utility, but I'm not sure how to not make it dumb yet --- pyomo/contrib/cp/tests/test_debugging.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 pyomo/contrib/cp/tests/test_debugging.py diff --git a/pyomo/contrib/cp/tests/test_debugging.py b/pyomo/contrib/cp/tests/test_debugging.py new file mode 100644 index 00000000000..4561b5e9f04 --- /dev/null +++ b/pyomo/contrib/cp/tests/test_debugging.py @@ -0,0 +1,28 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.environ import ( + ConcreteModel, + Constraint, + Var, +) + +class TestCPDebugging(unittest.TestCase): + def test_debug_infeasibility(self): + m = ConcreteModel() + m.x = Var(domain=Integers, bounds=(2, 5)) + m.y = Var(domain=Integers, bounds=(7, 12)) + m.c = Constraint(expr=m.y <= m.x) + + # ESJ TODO: I don't know how to do this without a baseline, which we + # really don't want... From ea7c23704b7b97e08029e283891e8213f9e771bb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:12:23 -0600 Subject: [PATCH 0854/1178] Make ProblemSense an extension of the ObjectiveSense enum --- pyomo/common/enums.py | 102 +++++++++++++++++++++++++++++++++-- pyomo/opt/results/problem.py | 38 ++++--------- 2 files changed, 110 insertions(+), 30 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index c685843b41c..7f00e87a85f 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -10,8 +10,97 @@ # ___________________________________________________________________________ import enum +import itertools + + +class ExtendedEnumType(enum.EnumType): + """Metaclass for creating an :py:class:`Enum` that extends another Enum + + In general, :py:class:`Enum` classes are not extensible: that is, + they are frozen when defined and cannot be the base class of another + Enum. This Metaclass provides a workaround for creating a new Enum + that extends an existing enum. Members in the base Enum are all + present as members on the extended enum. + + Example + ------- + + .. testcode:: + :hide: + + import enum + from pyomo.common.enums import ExtendedEnumType + + .. testcode:: + + class ObjectiveSense(enum.IntEnum): + minimize = 1 + maximize = -1 + + class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + .. doctest:: + + >>> list(ProblemSense) + [, , ] + >>> ProblemSense.unknown + + >>> ProblemSense.maximize + + >>> ProblemSense(0) + + >>> ProblemSense(1) + + >>> ProblemSense('unknown') + + >>> ProblemSense('maximize') + + >>> hasattr(ProblemSense, 'minimize') + True + >>> hasattr(ProblemSense, 'unknown') + True + >>> ProblemSense.minimize is ObjectiveSense.minimize + True + + """ + + def __getattr__(cls, attr): + try: + return getattr(cls.__base_enum__, attr) + except: + return super().__getattr__(attr) + + def __iter__(cls): + # The members of this Enum are the base enum members joined with + # the local members + return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) + + def __instancecheck__(cls, instance): + if cls.__subclasscheck__(type(instance)): + return True + # Also pretend that members of the extended enum are subclasses + # of the __base_enum__. This is needed to circumvent error + # checking in enum.__new__ (e.g., for `ProblemSense('minimize')`) + return cls.__base_enum__.__subclasscheck__(type(instance)) + + def _missing_(cls, value): + # Support attribute lookup by value or name + for attr in ('value', 'name'): + for member in cls: + if getattr(member, attr) == value: + return member + return None + + def __new__(metacls, cls, bases, classdict, **kwds): + # Support lookup by name - but only if the new Enum doesn't + # specify it's own implementation of _missing_ + if '_missing_' not in classdict: + classdict['_missing_'] = classmethod(ExtendedEnumType._missing_) + return super().__new__(metacls, cls, bases, classdict, **kwds) -from pyomo.common.deprecation import RenamedClass class ObjectiveSense(enum.IntEnum): """Flag indicating if an objective is minimizing (1) or maximizing (-1). @@ -21,6 +110,7 @@ class ObjectiveSense(enum.IntEnum): consistent with some solvers (notably Gurobi). """ + minimize = 1 maximize = -1 @@ -32,7 +122,13 @@ class ObjectiveSense(enum.IntEnum): def __str__(self): return self.name -minimize = ObjectiveSense.minimize -maximize = ObjectiveSense.maximize + @classmethod + def _missing_(cls, value): + for member in cls: + if member.name == value: + return member + return None +minimize = ObjectiveSense.minimize +maximize = ObjectiveSense.maximize diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index e35bb155355..34da8f91918 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -13,32 +13,16 @@ from pyomo.opt.results.container import MapContainer from pyomo.common.deprecation import deprecated, deprecation_warning -from pyomo.common.enums import ObjectiveSense - - -class ProblemSenseType(type): - @deprecated( - "pyomo.opt.results.problem.ProblemSense has been replaced by " - "pyomo.common.enums.ObjectiveSense", - version="6.7.2.dev0", - ) - def __getattr__(cls, attr): - if attr == 'minimize': - return ObjectiveSense.minimize - if attr == 'maximize': - return ObjectiveSense.maximize - if attr == 'unknown': - deprecation_warning( - "ProblemSense.unknown is no longer an allowable option. " - "Mapping 'unknown' to 'minimize'", - version="6.7.2.dev0", - ) - return ObjectiveSense.minimize - raise AttributeError(attr) - - -class ProblemSense(metaclass=ProblemSenseType): - pass +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense + + +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + def __str__(self): + return self.name class ProblemInformation(MapContainer): @@ -54,4 +38,4 @@ def __init__(self): self.declare('number_of_integer_variables') self.declare('number_of_continuous_variables') self.declare('number_of_nonzeros') - self.declare('sense', value=ProblemSense.minimize, required=True) + self.declare('sense', value=ProblemSense.unknown, required=True) From 297ce1d1f5f0a2d64c61a121ae35d63fd3f1509e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:27:57 -0600 Subject: [PATCH 0855/1178] Switch Objective.sense to store ObjectiveSense enum values --- pyomo/core/__init__.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/objective.py | 26 ++++---------------------- pyomo/core/kernel/objective.py | 11 ++--------- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index bce79faacc5..f0d168d98f9 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -101,7 +101,7 @@ BooleanValue, native_logical_values, ) -from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.base import minimize, maximize from pyomo.core.base.config import PyomoOptions from pyomo.core.base.expression import Expression diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 4bbd0c9dc44..df5ce743888 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -12,6 +12,7 @@ # TODO: this import is for historical backwards compatibility and should # probably be removed from pyomo.common.collections import ComponentMap +from pyomo.common.enums import minimize, maximize from pyomo.core.expr.symbol_map import SymbolMap from pyomo.core.expr.numvalue import ( @@ -33,7 +34,6 @@ BooleanValue, native_logical_values, ) -from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base.config import PyomoOptions from pyomo.core.base.expression import Expression, _ExpressionData diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fcc63755f2b..10cc853dafb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -15,6 +15,7 @@ from pyomo.common.pyomo_typing import overload from pyomo.common.deprecation import RenamedClass +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer @@ -35,7 +36,6 @@ IndexedCallInitializer, CountedCallInitializer, ) -from pyomo.core.base import minimize, maximize logger = logging.getLogger('pyomo.core') @@ -152,14 +152,7 @@ def __init__(self, expr=None, sense=minimize, component=None): self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET self._active = True - self._sense = sense - - if (self._sense != minimize) and (self._sense != maximize): - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) def set_value(self, expr): if expr is None: @@ -182,14 +175,7 @@ def sense(self, sense): def set_sense(self, sense): """Set the sense (direction) of this objective.""" - if sense in {minimize, maximize}: - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) @ModelComponentFactory.register("Expressions that are minimized or maximized.") @@ -353,11 +339,7 @@ def _pprint(self): ], self._data.items(), ("Active", "Sense", "Expression"), - lambda k, v: [ - v.active, - ("minimize" if (v.sense == minimize) else "maximize"), - v.expr, - ], + lambda k, v: [v.active, v.sense, v.expr], ) def display(self, prefix="", ostream=None): diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index 840c7cfd7e0..ac6f22d07d3 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.enums import minimize, maximize +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.core.expr.numvalue import as_numeric from pyomo.core.kernel.base import _abstract_readwrite_property from pyomo.core.kernel.container_utils import define_simple_containers @@ -81,14 +81,7 @@ def sense(self): @sense.setter def sense(self, sense): """Set the sense (direction) of this objective.""" - if (sense == minimize) or (sense == maximize): - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of: " - "[minimize (%s), maximize (%s)]. Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) # inserts class definitions for simple _tuple, _list, and From ab59255c9a574d40a63c8bfdd827efe15b8f7639 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:42:00 -0600 Subject: [PATCH 0856/1178] Remove reserences to ProblemSense --- pyomo/contrib/mindtpy/algorithm_base_class.py | 12 ++---------- pyomo/contrib/mindtpy/util.py | 1 - .../pynumero/algorithms/solvers/cyipopt_solver.py | 5 ++--- pyomo/solvers/plugins/solvers/CBCplugin.py | 12 ++++++------ pyomo/solvers/plugins/solvers/CPLEX.py | 14 +++++++------- pyomo/solvers/plugins/solvers/GAMS.py | 7 ++----- pyomo/solvers/plugins/solvers/GLPK.py | 8 +++----- pyomo/solvers/plugins/solvers/GUROBI.py | 8 ++++---- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 9 ++------- pyomo/solvers/tests/checks/test_CBCplugin.py | 14 +++++++------- 10 files changed, 35 insertions(+), 55 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 785a89d8982..8c703f8d842 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -27,13 +27,7 @@ from operator import itemgetter from pyomo.common.errors import DeveloperError from pyomo.solvers.plugins.solvers.gurobi_direct import gurobipy -from pyomo.opt import ( - SolverFactory, - SolverResults, - ProblemSense, - SolutionStatus, - SolverStatus, -) +from pyomo.opt import SolverFactory, SolverResults, SolutionStatus, SolverStatus from pyomo.core import ( minimize, maximize, @@ -633,9 +627,7 @@ def process_objective(self, update_var_con_list=True): raise ValueError('Model has multiple active objectives.') else: main_obj = active_objectives[0] - self.results.problem.sense = ( - ProblemSense.minimize if main_obj.sense == 1 else ProblemSense.maximize - ) + self.results.problem.sense = main_obj.sense self.objective_sense = main_obj.sense # Move the objective to the constraints if it is nonlinear or move_objective is True. diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 1543497838f..7345af8a3e2 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -29,7 +29,6 @@ from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr import pyomo.core.expr as EXPR -from pyomo.opt import ProblemSense from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.util.model_size import build_model_size_report from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index 53616298415..0999550711c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -65,7 +65,7 @@ from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.common.timing import TicTocTimer from pyomo.core.base import Block, Objective, minimize -from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense +from pyomo.opt import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.results.solution import Solution logger = logging.getLogger(__name__) @@ -447,11 +447,10 @@ def solve(self, model, **kwds): results.problem.name = model.name obj = next(model.component_data_objects(Objective, active=True)) + results.problem.sense = obj.sense if obj.sense == minimize: - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = info["obj_val"] else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = info["obj_val"] results.problem.number_of_objectives = 1 results.problem.number_of_constraints = ng diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index eb6c2c2e1bd..f22fb117c8b 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -16,6 +16,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager @@ -29,7 +30,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import SystemCallSolver @@ -443,7 +443,7 @@ def process_logfile(self): # # Parse logfile lines # - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize results.problem.name = None optim_value = float('inf') lower_bound = None @@ -578,7 +578,7 @@ def process_logfile(self): 'CoinLpIO::readLp(): Maximization problem reformulated as minimization' in ' '.join(tokens) ): - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: @@ -752,9 +752,9 @@ def process_logfile(self): "maxIterations parameter." ) soln.gap = gap - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: upper_bound = optim_value - elif results.problem.sense == ProblemSense.maximize: + elif results.problem.sense == maximize: _ver = self.version() if _ver and _ver[:3] < (2, 10, 2): optim_value *= -1 @@ -824,7 +824,7 @@ def process_soln_file(self, results): INPUT = [] _ver = self.version() - invert_objective_sense = results.problem.sense == ProblemSense.maximize and ( + invert_objective_sense = results.problem.sense == maximize and ( _ver and _ver[:3] < (2, 10, 2) ) diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index 9f876b2d0f8..3a08257c87c 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -17,6 +17,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -547,9 +547,9 @@ def process_logfile(self): ): # CPLEX 11.2 and subsequent has two Nonzeros sections. results.problem.number_of_nonzeros = int(tokens[2]) elif len(tokens) >= 5 and tokens[4] == "MINIMIZE": - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif len(tokens) >= 5 and tokens[4] == "MAXIMIZE": - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize elif ( len(tokens) >= 4 and tokens[0] == "Solution" @@ -859,9 +859,9 @@ def process_soln_file(self, results): else: sense = tokens[0].lower() if sense in ['max', 'maximize']: - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize if sense in ['min', 'minimize']: - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize break tINPUT.close() @@ -952,7 +952,7 @@ def process_soln_file(self, results): ) if primal_feasible == 1: soln.status = SolutionStatus.feasible - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = soln.objective[ '__default_objective__' ]['Value'] @@ -964,7 +964,7 @@ def process_soln_file(self, results): soln.status = SolutionStatus.infeasible if self._best_bound is not None: - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.lower_bound = self._best_bound else: results.problem.upper_bound = self._best_bound diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index c0bab4dc23e..035bd0b7603 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -36,7 +36,6 @@ Solution, SolutionStatus, TerminationCondition, - ProblemSense, ) from pyomo.common.dependencies import attempt_import @@ -422,11 +421,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = t1.out_db["OBJVAL"].find_record().value + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) @@ -984,11 +982,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = stat_vars["OBJVAL"] + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index e6d8576489d..c8d5bc14237 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -19,6 +19,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.opt import ( SolverFactory, @@ -28,7 +29,6 @@ SolverResults, TerminationCondition, SolutionStatus, - ProblemSense, ) from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver @@ -308,10 +308,8 @@ def process_soln_file(self, results): ): raise ValueError - self.is_integer = 'mip' == ptype and True or False - prob.sense = ( - 'min' == psense and ProblemSense.minimize or ProblemSense.maximize - ) + self.is_integer = 'mip' == ptype + prob.sense = minimize if 'min' == psense else maximize prob.number_of_constraints = prows prob.number_of_nonzeros = pnonz prob.number_of_variables = pcols diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index c8b0912970e..3a3a4d52322 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -18,6 +18,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.fileutils import this_file_dir from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -472,7 +472,7 @@ def process_soln_file(self, results): soln.objective['__default_objective__'] = { 'Value': float(tokens[1]) } - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = float(tokens[1]) else: results.problem.lower_bound = float(tokens[1]) @@ -514,9 +514,9 @@ def process_soln_file(self, results): elif section == 1: if tokens[0] == 'sense': if tokens[1] == 'minimize': - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif tokens[1] == 'maximize': - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize else: try: val = eval(tokens[1]) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index fd69954b428..6940ad7b5fe 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -20,12 +20,7 @@ from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory -from pyomo.opt.results import ( - SolverStatus, - TerminationCondition, - SolutionStatus, - ProblemSense, -) +from pyomo.opt.results import SolverStatus, TerminationCondition, SolutionStatus from pyomo.opt.solver import SystemCallSolver import logging @@ -374,7 +369,7 @@ def _postsolve(self): if len(results.solution) > 0: results.solution(0).status = SolutionStatus.optimal try: - if results.problem.sense == ProblemSense.minimize: + if results.solver.primal_bound < results.solver.dual_bound: results.problem.lower_bound = results.solver.primal_bound else: results.problem.upper_bound = results.solver.primal_bound diff --git a/pyomo/solvers/tests/checks/test_CBCplugin.py b/pyomo/solvers/tests/checks/test_CBCplugin.py index 2ea0e55c5f4..ad8846509ea 100644 --- a/pyomo/solvers/tests/checks/test_CBCplugin.py +++ b/pyomo/solvers/tests/checks/test_CBCplugin.py @@ -29,7 +29,7 @@ maximize, minimize, ) -from pyomo.opt import SolverFactory, ProblemSense, TerminationCondition, SolverStatus +from pyomo.opt import SolverFactory, TerminationCondition, SolverStatus from pyomo.solvers.plugins.solvers.CBCplugin import CBCSHELL cbc_available = SolverFactory('cbc', solver_io='lp').available(exception_flag=False) @@ -62,7 +62,7 @@ def test_infeasible_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -81,7 +81,7 @@ def test_unbounded_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -99,7 +99,7 @@ def test_optimal_lp(self): self.assertEqual(0.0, results.problem.lower_bound) self.assertEqual(0.0, results.problem.upper_bound) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) @@ -118,7 +118,7 @@ def test_infeasible_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -134,7 +134,7 @@ def test_unbounded_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -159,7 +159,7 @@ def test_optimal_mip(self): self.assertEqual(1.0, results.problem.upper_bound) self.assertEqual(results.problem.number_of_binary_variables, 2) self.assertEqual(results.problem.number_of_integer_variables, 4) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) From f80ff256c0da8a65f8be956e011246dc16f15583 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:44:22 -0600 Subject: [PATCH 0857/1178] Report both lower and upper bounds from SCIP --- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 2 ++ pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index 6940ad7b5fe..c309ad29d96 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -371,7 +371,9 @@ def _postsolve(self): try: if results.solver.primal_bound < results.solver.dual_bound: results.problem.lower_bound = results.solver.primal_bound + results.problem.upper_bound = results.solver.dual_bound else: + results.problem.lower_bound = results.solver.dual_bound results.problem.upper_bound = results.solver.primal_bound except AttributeError: """ diff --git a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline index a3eb9ffacec..976e4a1b82e 100644 --- a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline +++ b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline @@ -1,7 +1,7 @@ { "Problem": [ { - "Lower bound": -Infinity, + "Lower bound": 1.0, "Number of constraints": 0, "Number of objectives": 1, "Number of variables": 1, From 2f59e44f0eb0e1f360ee41fd3daebaba3404f7c3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 12:26:12 -0600 Subject: [PATCH 0858/1178] Updating baselines; ironically this makes the expected output actually match what is advertised in the Book --- examples/pyomobook/pyomo-components-ch/obj_declaration.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt index 607586a1fb3..e4d4b02a252 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt @@ -55,7 +55,7 @@ Model unknown None value x[Q] + 2*x[R] -1 +minimize 6.5 Model unknown From e1ad8e55b1b8ce26a4695f275b371f67c18062a8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 13:04:43 -0600 Subject: [PATCH 0859/1178] Adding portability fixes for Python<3.11 --- pyomo/common/enums.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 7f00e87a85f..ee934acd35f 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -11,9 +11,14 @@ import enum import itertools +import sys +if sys.version_info[:2] < (3, 11): + _EnumType = enum.EnumMeta +else: + _EnumType = enum.EnumType -class ExtendedEnumType(enum.EnumType): +class ExtendedEnumType(_EnumType): """Metaclass for creating an :py:class:`Enum` that extends another Enum In general, :py:class:`Enum` classes are not extensible: that is, From 4aa861cef0c91a5d335cb83d358a8ecf0074fb3c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 13:50:58 -0600 Subject: [PATCH 0860/1178] NFC: apply black --- pyomo/common/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index ee934acd35f..7de8b13b81f 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -18,6 +18,7 @@ else: _EnumType = enum.EnumType + class ExtendedEnumType(_EnumType): """Metaclass for creating an :py:class:`Enum` that extends another Enum From d095e2409ded5cc027fa50e0b07b5bb17b77c06f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:14:10 -0600 Subject: [PATCH 0861/1178] Integrate common.enums into online docs --- .../library_reference/common/enums.rst | 7 +++++ .../library_reference/common/index.rst | 1 + pyomo/common/enums.py | 26 +++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 doc/OnlineDocs/library_reference/common/enums.rst diff --git a/doc/OnlineDocs/library_reference/common/enums.rst b/doc/OnlineDocs/library_reference/common/enums.rst new file mode 100644 index 00000000000..5ed2dbb1e80 --- /dev/null +++ b/doc/OnlineDocs/library_reference/common/enums.rst @@ -0,0 +1,7 @@ + +pyomo.common.enums +================== + +.. automodule:: pyomo.common.enums + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/library_reference/common/index.rst b/doc/OnlineDocs/library_reference/common/index.rst index c9c99008250..c03436600f2 100644 --- a/doc/OnlineDocs/library_reference/common/index.rst +++ b/doc/OnlineDocs/library_reference/common/index.rst @@ -11,6 +11,7 @@ or rely on any other parts of Pyomo. config.rst dependencies.rst deprecation.rst + enums.rst errors.rst fileutils.rst formatting.rst diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 7de8b13b81f..9988beedbff 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -9,6 +9,23 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +"""This module provides standard :py:class:`enum.Enum` definitions used in +Pyomo, along with additional utilities for working with custom Enums + +Utilities: + +.. autosummary:: + + ExtendedEnumType + +Standard Enums: + +.. autosummary:: + + ObjectiveSense + +""" + import enum import itertools import sys @@ -20,9 +37,9 @@ class ExtendedEnumType(_EnumType): - """Metaclass for creating an :py:class:`Enum` that extends another Enum + """Metaclass for creating an :py:class:`enum.Enum` that extends another Enum - In general, :py:class:`Enum` classes are not extensible: that is, + In general, :py:class:`enum.Enum` classes are not extensible: that is, they are frozen when defined and cannot be the base class of another Enum. This Metaclass provides a workaround for creating a new Enum that extends an existing enum. Members in the base Enum are all @@ -84,6 +101,11 @@ def __iter__(cls): # the local members return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) + def __contains__(cls, member): + # This enum "containts" both it's local members and the members + # in the __base_enum__ (necessary for good auto-enum[sphinx] docs) + return super().__contains__(member) or member in cls.__base_enum__ + def __instancecheck__(cls, instance): if cls.__subclasscheck__(type(instance)): return True From 9a9c6843296b30bd45c0641c626736317cc785e2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 15:20:35 -0600 Subject: [PATCH 0862/1178] allow variables/constraints to be specified in same list in remove_nodes, but raise deprecation warning --- pyomo/contrib/incidence_analysis/interface.py | 28 +++++++++++++++++-- .../tests/test_interface.py | 18 ++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 0ed9b34b0f8..f8d7ea855d4 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -28,7 +28,7 @@ scipy as sp, plotly, ) -from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.contrib.incidence_analysis.config import get_config_from_kwds from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices @@ -911,7 +911,31 @@ def remove_nodes(self, variables=None, constraints=None): "Attempting to remove variables and constraints from cached " "incidence matrix,\nbut no incidence matrix has been cached." ) - variables, constraints = self._validate_input(variables, constraints) + + vars_to_validate = [] + cons_to_validate = [] + depr_msg = ( + "In IncidenceGraphInterface.remove_nodes, passing variables and" + " constraints in the same list is deprecated. Please separate your" + " variables and constraints and pass them in the order variables," + " constraints." + ) + for var in variables: + if var in self._con_index_map: + deprecation_warning(depr_msg, version="TBD") + cons_to_validate.append(var) + else: + vars_to_validate.append(var) + for con in constraints: + if con in self._var_index_map: + deprecation_warning(depr_msg, version="TBD") + vars_to_validate.append(con) + else: + cons_to_validate.append(con) + + variables, constraints = self._validate_input( + vars_to_validate, cons_to_validate + ) v_exclude = ComponentSet(variables) c_exclude = ComponentSet(constraints) vars_to_include = [v for v in self.variables if v not in v_exclude] diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 3b2439ed2af..816c8cbe3d3 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1471,6 +1471,24 @@ def test_remove_bad_node(self): with self.assertRaisesRegex(KeyError, "does not exist"): igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) + def test_remove_varcon_samelist_deprecated(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.eq = pyo.Constraint(pyo.PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == m.x[3] + m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3] + + igraph = IncidenceGraphInterface(m) + # This raises a deprecation warning. When the deprecated functionality + # is removed, this will fail, and this test should be updated accordingly. + igraph.remove_nodes([m.eq[1], m.x[1]]) + self.assertEqual(len(igraph.variables), 2) + self.assertEqual(len(igraph.constraints), 1) + + igraph.remove_nodes([], [m.eq[2], m.x[2]]) + self.assertEqual(len(igraph.variables), 1) + self.assertEqual(len(igraph.constraints), 0) + @unittest.skipUnless(networkx_available, "networkx is not available.") @unittest.skipUnless(scipy_available, "scipy is not available.") From 025e429ec66af22c0fa1fbaeae51f8d54a1d4421 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 15:23:39 -0600 Subject: [PATCH 0863/1178] rephrase "breaking change" as "deprecation" --- pyomo/contrib/incidence_analysis/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index f8d7ea855d4..64551788a8b 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -891,7 +891,7 @@ def remove_nodes(self, variables=None, constraints=None): .. note:: - **Breaking change in Pyomo vTBD** + **Deprecation in Pyomo vTBD** The pre-TBD implementation of ``remove_nodes`` allowed variables and constraints to remove to be specified in a single list. This made From f6b4ea93e8c25cd6b535051b0177c4620162a499 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:33:20 -0600 Subject: [PATCH 0864/1178] Add unit tests --- pyomo/common/enums.py | 4 +- pyomo/common/tests/test_enums.py | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 pyomo/common/tests/test_enums.py diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 9988beedbff..0dd65829026 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -83,10 +83,10 @@ class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): >>> hasattr(ProblemSense, 'minimize') True - >>> hasattr(ProblemSense, 'unknown') - True >>> ProblemSense.minimize is ObjectiveSense.minimize True + >>> ProblemSense.minimize in ProblemSense + True """ diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py new file mode 100644 index 00000000000..2d5ab01b6e3 --- /dev/null +++ b/pyomo/common/tests/test_enums.py @@ -0,0 +1,99 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum + +import pyomo.common.unittest as unittest + +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense + + +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + +class TestExtendedEnumType(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(ProblemSense), + [ProblemSense.unknown, ObjectiveSense.minimize, ObjectiveSense.maximize], + ) + + def test_isinstance(self): + self.assertIsInstance(ProblemSense.unknown, ProblemSense) + self.assertIsInstance(ProblemSense.minimize, ProblemSense) + self.assertIsInstance(ProblemSense.maximize, ProblemSense) + + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.unknown)) + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.minimize)) + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.maximize)) + + def test_getattr(self): + self.assertIs(ProblemSense.unknown, ProblemSense.unknown) + self.assertIs(ProblemSense.minimize, ObjectiveSense.minimize) + self.assertIs(ProblemSense.maximize, ObjectiveSense.maximize) + + def test_hasattr(self): + self.assertTrue(hasattr(ProblemSense, 'unknown')) + self.assertTrue(hasattr(ProblemSense, 'minimize')) + self.assertTrue(hasattr(ProblemSense, 'maximize')) + + def test_call(self): + self.assertIs(ProblemSense(0), ProblemSense.unknown) + self.assertIs(ProblemSense(1), ObjectiveSense.minimize) + self.assertIs(ProblemSense(-1), ObjectiveSense.maximize) + + self.assertIs(ProblemSense('unknown'), ProblemSense.unknown) + self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize) + self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize) + + with self.assertRaisesRegex( + ValueError, "'foo' is not a valid ProblemSense" + ): + ProblemSense('foo') + + def test_contains(self): + self.assertIn(ProblemSense.unknown, ProblemSense) + self.assertIn(ProblemSense.minimize, ProblemSense) + self.assertIn(ProblemSense.maximize, ProblemSense) + + self.assertNotIn(ProblemSense.unknown, ObjectiveSense) + self.assertIn(ProblemSense.minimize, ObjectiveSense) + self.assertIn(ProblemSense.maximize, ObjectiveSense) + +class TestObjectiveSense(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(ObjectiveSense), + [ObjectiveSense.minimize, ObjectiveSense.maximize], + ) + + def test_hasattr(self): + self.assertTrue(hasattr(ProblemSense, 'minimize')) + self.assertTrue(hasattr(ProblemSense, 'maximize')) + + def test_call(self): + self.assertIs(ObjectiveSense(1), ObjectiveSense.minimize) + self.assertIs(ObjectiveSense(-1), ObjectiveSense.maximize) + + self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize) + self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize) + + with self.assertRaisesRegex( + ValueError, "'foo' is not a valid ObjectiveSense" + ): + ObjectiveSense('foo') + + def test_str(self): + self.assertEqual(str(ObjectiveSense.minimize), 'minimize') + self.assertEqual(str(ObjectiveSense.maximize), 'maximize') From 98c960e937cdd30b1f7be2d5d00387a90fbe99af Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:39:27 -0600 Subject: [PATCH 0865/1178] NFC: fix typos --- pyomo/common/enums.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 0dd65829026..4d969bf7a9e 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -102,8 +102,8 @@ def __iter__(cls): return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) def __contains__(cls, member): - # This enum "containts" both it's local members and the members - # in the __base_enum__ (necessary for good auto-enum[sphinx] docs) + # This enum "contains" both its local members and the members in + # the __base_enum__ (necessary for good auto-enum[sphinx] docs) return super().__contains__(member) or member in cls.__base_enum__ def __instancecheck__(cls, instance): @@ -124,7 +124,7 @@ def _missing_(cls, value): def __new__(metacls, cls, bases, classdict, **kwds): # Support lookup by name - but only if the new Enum doesn't - # specify it's own implementation of _missing_ + # specify its own implementation of _missing_ if '_missing_' not in classdict: classdict['_missing_'] = classmethod(ExtendedEnumType._missing_) return super().__new__(metacls, cls, bases, classdict, **kwds) From 6c3245310aadd3211c2c7120d293adfb135517e2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:40:45 -0600 Subject: [PATCH 0866/1178] NFC: apply black --- pyomo/common/tests/test_enums.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py index 2d5ab01b6e3..52ee1c5abb3 100644 --- a/pyomo/common/tests/test_enums.py +++ b/pyomo/common/tests/test_enums.py @@ -57,9 +57,7 @@ def test_call(self): self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize) self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize) - with self.assertRaisesRegex( - ValueError, "'foo' is not a valid ProblemSense" - ): + with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"): ProblemSense('foo') def test_contains(self): @@ -71,11 +69,11 @@ def test_contains(self): self.assertIn(ProblemSense.minimize, ObjectiveSense) self.assertIn(ProblemSense.maximize, ObjectiveSense) + class TestObjectiveSense(unittest.TestCase): def test_members(self): self.assertEqual( - list(ObjectiveSense), - [ObjectiveSense.minimize, ObjectiveSense.maximize], + list(ObjectiveSense), [ObjectiveSense.minimize, ObjectiveSense.maximize] ) def test_hasattr(self): @@ -89,9 +87,7 @@ def test_call(self): self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize) self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize) - with self.assertRaisesRegex( - ValueError, "'foo' is not a valid ObjectiveSense" - ): + with self.assertRaisesRegex(ValueError, "'foo' is not a valid ObjectiveSense"): ObjectiveSense('foo') def test_str(self): From c7db40f70332d047fbb9197f8e2a9e694b4b34e6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 16:32:41 -0600 Subject: [PATCH 0867/1178] update remove_nodes tests for better coverage --- pyomo/contrib/incidence_analysis/tests/test_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 816c8cbe3d3..b0a9661aa54 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1466,7 +1466,10 @@ def test_remove_bad_node(self): with self.assertRaisesRegex(KeyError, "does not exist"): # Suppose we think something like this should work. We should get # an error, and not silently do nothing. - igraph.remove_nodes([m.x], [m.eq]) + igraph.remove_nodes([m.x], [m.eq[1]]) + + with self.assertRaisesRegex(KeyError, "does not exist"): + igraph.remove_nodes(None, [m.eq]) with self.assertRaisesRegex(KeyError, "does not exist"): igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) From d6af9b3b2d809ed45796fee016c2961acc9ecd89 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 20:42:18 -0600 Subject: [PATCH 0868/1178] only log deprecation warning once --- pyomo/contrib/incidence_analysis/interface.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 64551788a8b..ce6c3633e8d 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -920,15 +920,20 @@ def remove_nodes(self, variables=None, constraints=None): " variables and constraints and pass them in the order variables," " constraints." ) + if ( + any(var in self._con_index_map for var in variables) + or any(con in self._var_index_map for con in constraints) + ): + deprecation_warning(depr_msg, version="6.7.2.dev0") + # If we received variables/constraints in the same list, sort them. + # Any unrecognized objects will be caught by _validate_input. for var in variables: if var in self._con_index_map: - deprecation_warning(depr_msg, version="TBD") cons_to_validate.append(var) else: vars_to_validate.append(var) for con in constraints: if con in self._var_index_map: - deprecation_warning(depr_msg, version="TBD") vars_to_validate.append(con) else: cons_to_validate.append(con) From ac75f8380b59b9981e6853a37879d753a8676ef4 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 20:50:18 -0600 Subject: [PATCH 0869/1178] reformat --- pyomo/contrib/incidence_analysis/interface.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index ce6c3633e8d..8dee0539cb3 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -920,9 +920,8 @@ def remove_nodes(self, variables=None, constraints=None): " variables and constraints and pass them in the order variables," " constraints." ) - if ( - any(var in self._con_index_map for var in variables) - or any(con in self._var_index_map for con in constraints) + if any(var in self._con_index_map for var in variables) or any( + con in self._var_index_map for con in constraints ): deprecation_warning(depr_msg, version="6.7.2.dev0") # If we received variables/constraints in the same list, sort them. From ce34115f48d6610cda97e2438f91aae10886209a Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 08:32:43 -0400 Subject: [PATCH 0870/1178] Centralize function to merge AMPLFUNC and PYOMO_AMPLFUNC --- .../contrib/pynumero/interfaces/pyomo_nlp.py | 16 +++++++------- pyomo/solvers/amplfunc_merge.py | 21 +++++++++++++++++++ pyomo/solvers/plugins/solvers/ASL.py | 6 ++---- pyomo/solvers/plugins/solvers/IPOPT.py | 7 +++---- 4 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 pyomo/solvers/amplfunc_merge.py diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index ce148f50ecf..bfd22ede86b 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -22,6 +22,7 @@ import pyomo.core.base as pyo from pyomo.common.collections import ComponentMap from pyomo.common.env import CtypesEnviron +from pyomo.solvers.amplfunc_merge import amplfunc_merge from ..sparse.block_matrix import BlockMatrix from pyomo.contrib.pynumero.interfaces.ampl_nlp import AslNLP from pyomo.contrib.pynumero.interfaces.nlp import NLP @@ -92,13 +93,14 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - amplfunc_lines = os.environ.get("AMPLFUNC", "").split("\n") - existing = set(amplfunc_lines) - for line in os.environ.get("PYOMO_AMPLFUNC", "").split("\n"): - # Skip (a) empty lines and (b) lines we already have - if line != "" and line not in existing: - amplfunc_lines.append(line) - amplfunc = "\n".join(amplfunc_lines) + if 'PYOMO_AMPLFUNC' in os.environ: + if 'AMPLFUNC' in os.environ: + amplfunc = amplfunc_merge( + os.environ['AMPLFUNC'], os.environ['PYOMO_AMPLFUNC'] + ) + else: + amplfunc = os.environ['PYOMO_AMPLFUNC'] + with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py new file mode 100644 index 00000000000..72c6587f552 --- /dev/null +++ b/pyomo/solvers/amplfunc_merge.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def amplfunc_merge(amplfunc, pyomo_amplfunc): + """Merge two AMPLFUNC variable strings eliminating duplicate lines""" + amplfunc_lines = amplfunc.split("\n") + existing = set(amplfunc_lines) + for line in pyomo_amplfunc.split("\n"): + # Skip lines we already have + if line not in existing: + amplfunc_lines.append(line) + return "\n".join(amplfunc_lines) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 38a9fc1df58..6d3e08af259 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -23,6 +23,7 @@ from pyomo.opt.solver import SystemCallSolver from pyomo.core.kernel.block import IBlock from pyomo.solvers.mockmip import MockMIP +from pyomo.solvers.amplfunc_merge import amplfunc_merge from pyomo.core import TransformationFactory import logging @@ -160,10 +161,7 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - existing = set(env['AMPLFUNC'].split("\n")) - for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in existing: - env['AMPLFUNC'] += "\n" + line + env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 8f5190a4a07..a3c6b6beb28 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -21,6 +21,8 @@ from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver +from pyomo.solvers.amplfunc_merge import amplfunc_merge + import logging logger = logging.getLogger('pyomo.solvers') @@ -121,10 +123,7 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - existing = set(env['AMPLFUNC'].split("\n")) - for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in existing: - env['AMPLFUNC'] += "\n" + line + env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] From 11d11e0672ef963631d9c75fb5946dc683e6592c Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 09:29:56 -0400 Subject: [PATCH 0871/1178] Add tests --- pyomo/solvers/amplfunc_merge.py | 6 ++ .../tests/checks/test_amplfunc_merge.py | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 pyomo/solvers/tests/checks/test_amplfunc_merge.py diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 72c6587f552..88babc2f43c 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -12,10 +12,16 @@ def amplfunc_merge(amplfunc, pyomo_amplfunc): """Merge two AMPLFUNC variable strings eliminating duplicate lines""" + # Assume that the strings amplfunc and pyomo_amplfunc don't contain duplicates + # Assume that the path separator is correct for the OS so we don't need to + # worry about comparing Unix and Windows paths. amplfunc_lines = amplfunc.split("\n") existing = set(amplfunc_lines) for line in pyomo_amplfunc.split("\n"): # Skip lines we already have if line not in existing: amplfunc_lines.append(line) + # Remove empty lines which could happen if one or both of the strings is + # empty or there are two new lines in a row for whatever reason. + amplfunc_lines = [s for s in amplfunc_lines if s != ""] return "\n".join(amplfunc_lines) diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py new file mode 100644 index 00000000000..de31720010c --- /dev/null +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -0,0 +1,93 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.solvers.amplfunc_merge import amplfunc_merge + + +class TestAMPLFUNCMerge(unittest.TestCase): + def test_merge_no_dup(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l2.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 3) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + self.assertEqual(sm_list[2], "my/place/l2.so") + + def test_merge_empty1(self): + s1 = "" + s2 = "my/place/l2.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty2(self): + s1 = "my/place/l2.so" + s2 = "" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty_both(self): + s1 = "" + s2 = "" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + + def test_merge_bad_type(self): + self.assertRaises(AttributeError, amplfunc_merge, "", 3) + self.assertRaises(AttributeError, amplfunc_merge, 3, "") + self.assertRaises(AttributeError, amplfunc_merge, 3, 3) + self.assertRaises(AttributeError, amplfunc_merge, None, "") + self.assertRaises(AttributeError, amplfunc_merge, "", None) + self.assertRaises(AttributeError, amplfunc_merge, 2.3, "") + self.assertRaises(AttributeError, amplfunc_merge, "", 2.3) + + def test_merge_duplicate1(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l1.so\nanother/place/l1.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_duplicate2(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l1.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_extra_linebreaks(self): + s1 = "\nmy/place/l1.so\nanother/place/l1.so\n" + s2 = "\nmy/place/l1.so\n\n" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") From 30ffed7a4ee79435bd472f7846c0d95f67768712 Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 10:13:25 -0400 Subject: [PATCH 0872/1178] Fix error in pyomo_nlp missing undefined amplfunc --- .../contrib/pynumero/interfaces/pyomo_nlp.py | 8 +- pyomo/solvers/amplfunc_merge.py | 6 +- pyomo/solvers/plugins/solvers/ASL.py | 8 +- pyomo/solvers/plugins/solvers/IPOPT.py | 8 +- .../tests/checks/test_amplfunc_merge.py | 114 +++++++++++++++--- 5 files changed, 110 insertions(+), 34 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index bfd22ede86b..e12d0cf568b 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -93,13 +93,7 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - if 'PYOMO_AMPLFUNC' in os.environ: - if 'AMPLFUNC' in os.environ: - amplfunc = amplfunc_merge( - os.environ['AMPLFUNC'], os.environ['PYOMO_AMPLFUNC'] - ) - else: - amplfunc = os.environ['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(os.environ) with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 88babc2f43c..4c77f080ca1 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -def amplfunc_merge(amplfunc, pyomo_amplfunc): +def amplfunc_string_merge(amplfunc, pyomo_amplfunc): """Merge two AMPLFUNC variable strings eliminating duplicate lines""" # Assume that the strings amplfunc and pyomo_amplfunc don't contain duplicates # Assume that the path separator is correct for the OS so we don't need to @@ -25,3 +25,7 @@ def amplfunc_merge(amplfunc, pyomo_amplfunc): # empty or there are two new lines in a row for whatever reason. amplfunc_lines = [s for s in amplfunc_lines if s != ""] return "\n".join(amplfunc_lines) + +def amplfunc_merge(env): + """Merge AMPLFUNC and PYOMO_AMPLFuNC in an environment var dict""" + return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) \ No newline at end of file diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 6d3e08af259..bb8174a013e 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -159,11 +159,9 @@ def create_command_line(self, executable, problem_files): # Pyomo/Pyomo) with any user-specified external function # libraries # - if 'PYOMO_AMPLFUNC' in env: - if 'AMPLFUNC' in env: - env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) - else: - env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(env) + if amplfunc: + env['AMPLFUNC'] = amplfunc cmd = [executable, problem_files[0], '-AMPL'] if self._timer: diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index a3c6b6beb28..21045cb7b4f 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -121,11 +121,9 @@ def create_command_line(self, executable, problem_files): # Pyomo/Pyomo) with any user-specified external function # libraries # - if 'PYOMO_AMPLFUNC' in env: - if 'AMPLFUNC' in env: - env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) - else: - env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(env) + if amplfunc: + env['AMPLFUNC'] = amplfunc cmd = [executable, problem_files[0], '-AMPL'] if self._timer: diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py index de31720010c..fb7701e9282 100644 --- a/pyomo/solvers/tests/checks/test_amplfunc_merge.py +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -10,14 +10,14 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.solvers.amplfunc_merge import amplfunc_merge +from pyomo.solvers.amplfunc_merge import amplfunc_string_merge, amplfunc_merge -class TestAMPLFUNCMerge(unittest.TestCase): +class TestAMPLFUNCStringMerge(unittest.TestCase): def test_merge_no_dup(self): s1 = "my/place/l1.so\nanother/place/l1.so" s2 = "my/place/l2.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 3) # The order of lines should be maintained with the second string @@ -29,7 +29,7 @@ def test_merge_no_dup(self): def test_merge_empty1(self): s1 = "" s2 = "my/place/l2.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") @@ -37,7 +37,7 @@ def test_merge_empty1(self): def test_merge_empty2(self): s1 = "my/place/l2.so" s2 = "" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") @@ -45,24 +45,24 @@ def test_merge_empty2(self): def test_merge_empty_both(self): s1 = "" s2 = "" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "") def test_merge_bad_type(self): - self.assertRaises(AttributeError, amplfunc_merge, "", 3) - self.assertRaises(AttributeError, amplfunc_merge, 3, "") - self.assertRaises(AttributeError, amplfunc_merge, 3, 3) - self.assertRaises(AttributeError, amplfunc_merge, None, "") - self.assertRaises(AttributeError, amplfunc_merge, "", None) - self.assertRaises(AttributeError, amplfunc_merge, 2.3, "") - self.assertRaises(AttributeError, amplfunc_merge, "", 2.3) + self.assertRaises(AttributeError, amplfunc_string_merge, "", 3) + self.assertRaises(AttributeError, amplfunc_string_merge, 3, "") + self.assertRaises(AttributeError, amplfunc_string_merge, 3, 3) + self.assertRaises(AttributeError, amplfunc_string_merge, None, "") + self.assertRaises(AttributeError, amplfunc_string_merge, "", None) + self.assertRaises(AttributeError, amplfunc_string_merge, 2.3, "") + self.assertRaises(AttributeError, amplfunc_string_merge, "", 2.3) def test_merge_duplicate1(self): s1 = "my/place/l1.so\nanother/place/l1.so" s2 = "my/place/l1.so\nanother/place/l1.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) # The order of lines should be maintained with the second string @@ -73,7 +73,7 @@ def test_merge_duplicate1(self): def test_merge_duplicate2(self): s1 = "my/place/l1.so\nanother/place/l1.so" s2 = "my/place/l1.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) # The order of lines should be maintained with the second string @@ -84,10 +84,92 @@ def test_merge_duplicate2(self): def test_merge_extra_linebreaks(self): s1 = "\nmy/place/l1.so\nanother/place/l1.so\n" s2 = "\nmy/place/l1.so\n\n" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) # The order of lines should be maintained with the second string # following the first self.assertEqual(sm_list[0], "my/place/l1.so") self.assertEqual(sm_list[1], "another/place/l1.so") + +class TestAMPLFUNCMerge(unittest.TestCase): + def test_merge_no_dup(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + "PYOMO_AMPLFUNC": "my/place/l2.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 3) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + self.assertEqual(sm_list[2], "my/place/l2.so") + + def test_merge_empty1(self): + env = { + "AMPLFUNC": "", + "PYOMO_AMPLFUNC": "my/place/l2.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty2(self): + env = { + "AMPLFUNC": "my/place/l2.so", + "PYOMO_AMPLFUNC": "", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty_both(self): + env = { + "AMPLFUNC": "", + "PYOMO_AMPLFUNC": "", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + + def test_merge_duplicate1(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_no_pyomo(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_no_user(self): + env = { + "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_nothing(self): + env = {} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + From d4c9ddf35c28dbd2ce20f81e8fff485d3fec13b5 Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 10:20:44 -0400 Subject: [PATCH 0873/1178] Run black --- pyomo/solvers/amplfunc_merge.py | 3 ++- pyomo/solvers/tests/checks/test_amplfunc_merge.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 4c77f080ca1..9d127a94396 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -26,6 +26,7 @@ def amplfunc_string_merge(amplfunc, pyomo_amplfunc): amplfunc_lines = [s for s in amplfunc_lines if s != ""] return "\n".join(amplfunc_lines) + def amplfunc_merge(env): """Merge AMPLFUNC and PYOMO_AMPLFuNC in an environment var dict""" - return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) \ No newline at end of file + return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py index fb7701e9282..00885feb5a4 100644 --- a/pyomo/solvers/tests/checks/test_amplfunc_merge.py +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -92,6 +92,7 @@ def test_merge_extra_linebreaks(self): self.assertEqual(sm_list[0], "my/place/l1.so") self.assertEqual(sm_list[1], "another/place/l1.so") + class TestAMPLFUNCMerge(unittest.TestCase): def test_merge_no_dup(self): env = { @@ -172,4 +173,3 @@ def test_merge_nothing(self): sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "") - From 4849ec515cee9d7ea1fd48747dc5f087a56752cb Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 10:30:58 -0400 Subject: [PATCH 0874/1178] Run black again --- .../tests/checks/test_amplfunc_merge.py | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py index 00885feb5a4..2c819404d2f 100644 --- a/pyomo/solvers/tests/checks/test_amplfunc_merge.py +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -107,30 +107,21 @@ def test_merge_no_dup(self): self.assertEqual(sm_list[2], "my/place/l2.so") def test_merge_empty1(self): - env = { - "AMPLFUNC": "", - "PYOMO_AMPLFUNC": "my/place/l2.so", - } + env = {"AMPLFUNC": "", "PYOMO_AMPLFUNC": "my/place/l2.so"} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") def test_merge_empty2(self): - env = { - "AMPLFUNC": "my/place/l2.so", - "PYOMO_AMPLFUNC": "", - } + env = {"AMPLFUNC": "my/place/l2.so", "PYOMO_AMPLFUNC": ""} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") def test_merge_empty_both(self): - env = { - "AMPLFUNC": "", - "PYOMO_AMPLFUNC": "", - } + env = {"AMPLFUNC": "", "PYOMO_AMPLFUNC": ""} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) @@ -148,9 +139,7 @@ def test_merge_duplicate1(self): self.assertEqual(sm_list[1], "another/place/l1.so") def test_merge_no_pyomo(self): - env = { - "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", - } + env = {"AMPLFUNC": "my/place/l1.so\nanother/place/l1.so"} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) @@ -158,9 +147,7 @@ def test_merge_no_pyomo(self): self.assertEqual(sm_list[1], "another/place/l1.so") def test_merge_no_user(self): - env = { - "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", - } + env = {"PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so"} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) From 24d3baf9b174beab9f34f39e02010fc87194a193 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 4 Apr 2024 09:03:56 -0600 Subject: [PATCH 0875/1178] replace TBD with dev version in docstring --- pyomo/contrib/incidence_analysis/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 8dee0539cb3..0f47e03d0a2 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -891,9 +891,9 @@ def remove_nodes(self, variables=None, constraints=None): .. note:: - **Deprecation in Pyomo vTBD** + **Deprecation in Pyomo v6.7.2.dev0** - The pre-TBD implementation of ``remove_nodes`` allowed variables and + The pre-6.7.2.dev0 implementation of ``remove_nodes`` allowed variables and constraints to remove to be specified in a single list. This made error checking difficult, and indeed, if invalid components were provided, we carried on silently instead of throwing an error or From b830879053cdbed2a6746bf0bf7b4af2f15ac073 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:22:04 -0600 Subject: [PATCH 0876/1178] adding 'synchronize' expression --- pyomo/contrib/cp/__init__.py | 6 +++++- pyomo/contrib/cp/repn/docplex_writer.py | 6 ++++++ .../cp/scheduling_expr/scheduling_logic.py | 15 +++++++++++++ pyomo/contrib/cp/tests/test_docplex_walker.py | 21 +++++++++++++++++++ .../cp/tests/test_sequence_expressions.py | 15 +++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index 96ef037853a..d206fe95251 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -25,7 +25,11 @@ before_in_sequence, predecessor_to, ) -from pyomo.contrib.cp.scheduling_expr.scheduling_logic import alternative, spans +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + alternative, + spans, + synchronize, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 93b9974434a..98d3e07e8ed 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -39,6 +39,7 @@ from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( AlternativeExpression, SpanExpression, + SynchronizeExpression, ) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, @@ -992,6 +993,10 @@ def _handle_alternative_expression_node(visitor, node, *args): return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) +def _handle_synchronize_expression_node(visitor, node, *args): + return _GENERAL, cp.synchronize(args[0][1], [arg[1] for arg in args[1:]]) + + class LogicalToDoCplex(StreamBasedExpressionVisitor): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, @@ -1040,6 +1045,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): PredecessorToExpression: _handle_predecessor_to_expression_node, SpanExpression: _handle_span_expression_node, AlternativeExpression: _handle_alternative_expression_node, + SynchronizeExpression: _handle_synchronize_expression_node, } _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index b28d536b594..3556c3083fd 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -36,6 +36,15 @@ def _to_string(self, values, verbose, smap): return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) +class SynchronizeExpression(NaryBooleanExpression): + """ + + """ + + def _to_string(self, values, verbose, smap): + return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) + + def spans(*args): """Creates a new SpanExpression""" @@ -46,3 +55,9 @@ def alternative(*args): """Creates a new AlternativeExpression""" return AlternativeExpression(list(_flattened(args))) + + +def synchronize(*args): + """Creates a new SynchronizeExpression""" + + return SynchronizeExpression(list(_flattened(args))) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 4ad07c10ee7..cce7306d3f9 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -20,6 +20,7 @@ before_in_sequence, predecessor_to, alternative, + synchronize, ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, @@ -1555,6 +1556,26 @@ def test_alternative(self): expr[1].equals(cp.alternative(whole_enchilada, [iv[i] for i in [1, 2, 3]])) ) + def test_synchronize(self): + m = self.get_model() + e = synchronize(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue( + expr[1].equals(cp.synchronize(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_CumulFuncExpressions(CommonTest): diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 62c868abfaf..b676881e379 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -15,8 +15,10 @@ from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( AlternativeExpression, SpanExpression, + SynchronizeExpression, alternative, spans, + synchronize, ) from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( NoOverlapExpression, @@ -159,3 +161,16 @@ def test_alternative(self): self.assertIs(e.args[i], m.iv[i]) self.assertEqual(str(e), "alternative(whole_enchilada, [iv[1], iv[2], iv[3]])") + + def test_synchronize(self): + m = self.make_model() + e = synchronize(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + self.assertIsInstance(e, SynchronizeExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "synchronize(whole_enchilada, [iv[1], iv[2], iv[3]])") From 535dda6fbcf876b8ad31f226fd26748e6dc4e81f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:23:11 -0600 Subject: [PATCH 0877/1178] black --- pyomo/contrib/cp/scheduling_expr/scheduling_logic.py | 4 +--- pyomo/contrib/cp/tests/test_debugging.py | 7 ++----- pyomo/contrib/cp/tests/test_docplex_walker.py | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index 3556c3083fd..98e0c1ceabd 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -37,9 +37,7 @@ def _to_string(self, values, verbose, smap): class SynchronizeExpression(NaryBooleanExpression): - """ - - """ + """ """ def _to_string(self, values, verbose, smap): return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) diff --git a/pyomo/contrib/cp/tests/test_debugging.py b/pyomo/contrib/cp/tests/test_debugging.py index 4561b5e9f04..8e24e545724 100644 --- a/pyomo/contrib/cp/tests/test_debugging.py +++ b/pyomo/contrib/cp/tests/test_debugging.py @@ -11,11 +11,8 @@ import pyomo.common.unittest as unittest -from pyomo.environ import ( - ConcreteModel, - Constraint, - Var, -) +from pyomo.environ import ConcreteModel, Constraint, Var + class TestCPDebugging(unittest.TestCase): def test_debug_infeasibility(self): diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index cce7306d3f9..d14e0bc2d6f 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -248,7 +248,7 @@ def test_monomial_expressions(self): const_expr = 3 * m.x nested_expr = (1 / m.p) * m.x pow_expr = (m.p ** (0.5)) * m.x - + e = m.x * 4 expr = visitor.walk_expression((e, e, 0)) self.assertIn(id(m.x), visitor.var_map) @@ -1574,7 +1574,7 @@ def test_synchronize(self): self.assertTrue( expr[1].equals(cp.synchronize(whole_enchilada, [iv[i] for i in [1, 2, 3]])) - ) + ) @unittest.skipIf(not docplex_available, "docplex is not available") From 347b5950ba5e8515541783e6480e79c50d05ec88 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:24:33 -0600 Subject: [PATCH 0878/1178] standard_form: return objective list, offsets --- pyomo/repn/plugins/standard_form.py | 39 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index ea7b6a6a9e6..d09537e4eee 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -61,12 +61,16 @@ class LinearStandardFormInfo(object): Attributes ---------- - c : scipy.sparse.csr_array + c : scipy.sparse.csc_array The objective coefficients. Note that this is a sparse array and may contain multiple rows (for multiobjective problems). The objectives may be calculated by "c @ x" + c_offset : numpy.ndarray + + The list of objective constant offsets + A : scipy.sparse.csc_array The constraint coefficients. The constraint bodies may be @@ -89,6 +93,10 @@ class LinearStandardFormInfo(object): The list of Pyomo variable objects corresponding to columns in the `A` and `c` matrices. + objectives : List[_ObjectiveData] + + The list of Pyomo objective objects correcponding to the active objectives + eliminated_vars: List[Tuple[_VarData, NumericExpression]] The list of variables from the original model that do not appear @@ -101,12 +109,14 @@ class LinearStandardFormInfo(object): """ - def __init__(self, c, A, rhs, rows, columns, eliminated_vars): + def __init__(self, c, c_offset, A, rhs, rows, columns, objectives, eliminated_vars): self.c = c + self.c_offset = c_offset self.A = A self.rhs = rhs self.rows = rows self.columns = columns + self.objectives = objectives self.eliminated_vars = eliminated_vars @property @@ -305,21 +315,18 @@ def write(self, model): # # Process objective # - if not component_map[Objective]: - objectives = [Objective(expr=1)] - objectives[0].construct() - else: - objectives = [] - for blk in component_map[Objective]: - objectives.extend( - blk.component_data_objects( - Objective, active=True, descend_into=False, sort=sorter - ) + objectives = [] + for blk in component_map[Objective]: + objectives.extend( + blk.component_data_objects( + Objective, active=True, descend_into=False, sort=sorter ) + ) + obj_offset = [] obj_data = [] obj_index = [] obj_index_ptr = [0] - for i, obj in enumerate(objectives): + for obj in objectives: repn = visitor.walk_expression(obj.expr) if repn.nonlinear is not None: raise ValueError( @@ -328,8 +335,10 @@ def write(self, model): ) N = len(repn.linear) obj_data.append(np.fromiter(repn.linear.values(), float, N)) + obj_offset.append(repn.constant) if obj.sense == maximize: obj_data[-1] *= -1 + obj_offset[-1] *= -1 obj_index.append( np.fromiter(map(var_order.__getitem__, repn.linear), float, N) ) @@ -495,7 +504,9 @@ def write(self, model): else: eliminated_vars = [] - info = LinearStandardFormInfo(c, A, rhs, rows, columns, eliminated_vars) + info = LinearStandardFormInfo( + c, np.array(obj_offset), A, rhs, rows, columns, objectives, eliminated_vars + ) timer.toc("Generated linear standard form representation", delta=False) return info From 89556c3da721c10ad7390cc2be0a0cc07e1124db Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:25:19 -0600 Subject: [PATCH 0879/1178] standard_form: allow empty objectives, constraints --- pyomo/repn/plugins/standard_form.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index d09537e4eee..0211ba44387 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -465,13 +465,17 @@ def write(self, model): # Get the variable list columns = list(var_map.values()) # Convert the compiled data to scipy sparse matrices + if obj_data: + obj_data = np.concatenate(obj_data) + obj_index = np.concatenate(obj_index) c = scipy.sparse.csr_array( - (np.concatenate(obj_data), np.concatenate(obj_index), obj_index_ptr), - [len(obj_index_ptr) - 1, len(columns)], + (obj_data, obj_index, obj_index_ptr), [len(obj_index_ptr) - 1, len(columns)] ).tocsc() + if rows: + con_data = np.concatenate(con_data) + con_index = np.concatenate(con_index) A = scipy.sparse.csr_array( - (np.concatenate(con_data), np.concatenate(con_index), con_index_ptr), - [len(rows), len(columns)], + (con_data, con_index, con_index_ptr), [len(rows), len(columns)] ).tocsc() # Some variables in the var_map may not actually appear in the From e60c0dea7749100c94da6a697395d143e720f3c5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:31:24 -0600 Subject: [PATCH 0880/1178] gurobi_direct: support (partial) loading duals, reduced costs --- pyomo/contrib/solver/gurobi_direct.py | 77 ++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 1164686f0f1..f10cc8f619f 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -15,6 +15,7 @@ import os from pyomo.common.config import ConfigValue +from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import attempt_import from pyomo.common.shutdown import python_is_shutting_down from pyomo.common.tee import capture_output, TeeStream @@ -59,10 +60,13 @@ def __init__( class GurobiDirectSolutionLoader(SolutionLoaderBase): - def __init__(self, grb_model, grb_vars, pyo_vars): + def __init__(self, grb_model, grb_cons, grb_vars, pyo_cons, pyo_vars, pyo_obj): self._grb_model = grb_model + self._grb_cons = grb_cons self._grb_vars = grb_vars + self._pyo_cons = pyo_cons self._pyo_vars = pyo_vars + self._pyo_obj = pyo_obj GurobiDirect._num_instances += 1 def __del__(self): @@ -72,15 +76,70 @@ def __del__(self): GurobiDirect.release_license() def load_vars(self, vars_to_load=None, solution_number=0): - assert vars_to_load is None assert solution_number == 0 - for p_var, g_var in zip(self._pyo_vars, self._grb_vars.x.tolist()): + if self._grb_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.x.tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_val: var_val[0] in vars_to_load, iterator) + for p_var, g_var in iterator: p_var.set_value(g_var, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) - def get_primals(self, vars_to_load=None): - assert vars_to_load is None + def get_primals(self, vars_to_load=None, solution_number=0): assert solution_number == 0 - return ComponentMap(zip(self._pyo_vars, self._grb_vars.x.tolist())) + if self._grb_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.x.tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_val: var_val[0] in vars_to_load, iterator) + return ComponentMap(iterator) + + def get_duals(self, cons_to_load=None): + if self._grb_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid duals. Please ' + 'check the termination condition.' + ) + + def dedup(_iter): + last = None + for con_info_dual in _iter: + if not con_info_dual[1] and con_info_dual[0][0] is last: + continue + last = con_info_dual[0][0] + yield con_info_dual + + iterator = dedup(zip(self._pyo_cons, self._grb_cons.getAttr('Pi').tolist())) + if cons_to_load: + cons_to_load = set(cons_to_load) + iterator = filter( + lambda con_info_dual: con_info_dual[0][0] in cons_to_load, iterator + ) + return {con_info[0]: dual for con_info, dual in iterator} + + def get_reduced_costs(self, vars_to_load=None): + if self._grb_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid reduced costs. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.getAttr('Rc').tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_rc: var_rc[0] in vars_to_load, iterator) + return ComponentMap(iterator) class GurobiDirect(SolverBase): @@ -240,7 +299,11 @@ def solve(self, model, **kwds) -> Results: os.chdir(orig_cwd) res = self._postsolve( - timer, GurobiDirectSolutionLoader(gurobi_model, x, repn.columns) + timer, + config, + GurobiDirectSolutionLoader( + gurobi_model, A, x, repn.rows, repn.columns, repn.objectives + ), ) res.solver_configuration = config res.solver_name = 'Gurobi' From dbefc5cab0a824283b8ac00d92950e569a10760f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:39:44 -0600 Subject: [PATCH 0881/1178] gurobi_direct: do not store ephemeral config on instance; rename gprob --- pyomo/contrib/solver/gurobi_direct.py | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index f10cc8f619f..1d4b1871654 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -213,12 +213,13 @@ def version(self): def solve(self, model, **kwds) -> Results: start_timestamp = datetime.datetime.now(datetime.timezone.utc) - self._config = config = self.config(value=kwds, preserve_implicit=True) - StaleFlagManager.mark_all_as_stale() + config = self.config(value=kwds, preserve_implicit=True) if config.timer is None: config.timer = HierarchicalTimer() timer = config.timer + StaleFlagManager.mark_all_as_stale() + timer.start('compile_model') repn = LinearStandardFormCompiler().write(model, mixed_form=True) timer.stop('compile_model') @@ -256,8 +257,8 @@ def solve(self, model, **kwds) -> Results: try: orig_cwd = os.getcwd() - if self._config.working_dir: - os.chdir(self._config.working_dir) + if config.working_dir: + os.chdir(config.working_dir) with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): gurobi_model = gurobipy.Model() @@ -316,17 +317,15 @@ def solve(self, model, **kwds) -> Results: res.timing_info.timer = timer return res - def _postsolve(self, timer: HierarchicalTimer, loader): - config = self._config - - gprob = loader._grb_model - status = gprob.Status + def _postsolve(self, timer: HierarchicalTimer, config, loader): + grb_model = loader._grb_model + status = grb_model.Status results = Results() results.solution_loader = loader - results.timing_info.gurobi_time = gprob.Runtime + results.timing_info.gurobi_time = grb_model.Runtime - if gprob.SolCount > 0: + if grb_model.SolCount > 0: if status == gurobipy.GRB.OPTIMAL: results.solution_status = SolutionStatus.optimal else: @@ -349,30 +348,31 @@ def _postsolve(self, timer: HierarchicalTimer, loader): 'to bypass this error.' ) - results.incumbent_objective = None - results.objective_bound = None try: - results.incumbent_objective = gprob.ObjVal + if math.isfinite(grb_model.ObjVal): + results.incumbent_objective = grb_model.ObjVal + else: + results.incumbent_objective = None except (gurobipy.GurobiError, AttributeError): results.incumbent_objective = None try: - results.objective_bound = gprob.ObjBound + results.objective_bound = grb_model.ObjBound except (gurobipy.GurobiError, AttributeError): - if self._objective.sense == minimize: + if grb_model.ModelSense == OptimizationSense.minimize: results.objective_bound = -math.inf else: results.objective_bound = math.inf - if results.incumbent_objective is not None and not math.isfinite( results.incumbent_objective ): results.incumbent_objective = None + results.objective_bound = None - results.iteration_count = gprob.getAttr('IterCount') + results.iteration_count = grb_model.getAttr('IterCount') timer.start('load solution') if config.load_solutions: - if gprob.SolCount > 0: + if grb_model.SolCount > 0: results.solution_loader.load_vars() else: raise RuntimeError( From e11fd66b7c8abeff4aa7c9b728de252ec20741d9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:41:10 -0600 Subject: [PATCH 0882/1178] gurobi_direct: make var processing more efficient --- pyomo/contrib/solver/gurobi_direct.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 1d4b1871654..54ef2c5306e 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -237,20 +237,19 @@ def solve(self, model, **kwds) -> Results: _u = inf lb.append(_l) ub.append(_u) + CON = gurobipy.GRB.CONTINUOUS + BIN = gurobipy.GRB.BINARY + INT = gurobipy.GRB.INTEGER vtype = [ ( - gurobipy.GRB.CONTINUOUS + CON if v.is_continuous() - else ( - gurobipy.GRB.BINARY - if v.is_binary() - else gurobipy.GRB.INTEGER if v.is_integer() else '?' - ) + else (BIN if v.is_binary() else INT if v.is_integer() else '?') ) for v in repn.columns ] - sense_type = '>=<' - sense = [sense_type[r[1] + 1] for r in repn.rows] + sense_type = '=<>' # Note: ordering matches 0, 1, -1 + sense = [sense_type[r[1]] for r in repn.rows] timer.stop('prepare_matrices') ostreams = [io.StringIO()] + config.tee From 99a7efc2cbc93456c2a0e69850593444613b4a24 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:41:57 -0600 Subject: [PATCH 0883/1178] gurobi_direct: support models with no objectives --- pyomo/contrib/solver/gurobi_direct.py | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 54ef2c5306e..82491e88a42 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -266,10 +266,13 @@ def solve(self, model, **kwds) -> Results: len(repn.columns), lb=lb, ub=ub, - obj=repn.c.todense()[0], + obj=repn.c.todense()[0] if repn.c.shape[0] else 0, vtype=vtype, ) A = gurobi_model.addMConstr(repn.A, x, sense, repn.rhs) + if repn.c.shape[0]: + gurobi_model.setAttr('ObjCon', repn.c_offset[0]) + gurobi_model.setAttr('ModelSense', int(repn.objectives[0].sense)) # gurobi_model.update() timer.stop('transfer_model') @@ -347,23 +350,22 @@ def _postsolve(self, timer: HierarchicalTimer, config, loader): 'to bypass this error.' ) - try: - if math.isfinite(grb_model.ObjVal): - results.incumbent_objective = grb_model.ObjVal - else: + if loader._pyo_obj: + try: + if math.isfinite(grb_model.ObjVal): + results.incumbent_objective = grb_model.ObjVal + else: + results.incumbent_objective = None + except (gurobipy.GurobiError, AttributeError): results.incumbent_objective = None - except (gurobipy.GurobiError, AttributeError): - results.incumbent_objective = None - try: - results.objective_bound = grb_model.ObjBound - except (gurobipy.GurobiError, AttributeError): - if grb_model.ModelSense == OptimizationSense.minimize: - results.objective_bound = -math.inf - else: - results.objective_bound = math.inf - if results.incumbent_objective is not None and not math.isfinite( - results.incumbent_objective - ): + try: + results.objective_bound = grb_model.ObjBound + except (gurobipy.GurobiError, AttributeError): + if grb_model.ModelSense == OptimizationSense.minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + else: results.incumbent_objective = None results.objective_bound = None From 7e4938f8e4fe32eef50c1d4816c378d0e28b6413 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:42:19 -0600 Subject: [PATCH 0884/1178] NFC: wrap long line --- pyomo/contrib/solver/gurobi_direct.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 82491e88a42..ab5a07bc5f5 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -54,7 +54,8 @@ def __init__( ConfigValue( default=False, domain=bool, - description="If True, the values of the integer variables will be passed to Gurobi.", + description="If True, the current values of the integer variables " + "will be passed to Gurobi.", ), ) From 93590c6eabbb1c2f0b06592f6687d2feefb0243e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:42:45 -0600 Subject: [PATCH 0885/1178] gurobi_direct: add error checking for MO problems --- pyomo/contrib/solver/gurobi_direct.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index ab5a07bc5f5..e0e0d7d32b0 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -225,6 +225,12 @@ def solve(self, model, **kwds) -> Results: repn = LinearStandardFormCompiler().write(model, mixed_form=True) timer.stop('compile_model') + if len(repn.objectives) > 1: + raise ValueError( + f"The {self.__class__.__name__} solver only supports models " + f"with zero or one objectives (received {len(repn.objectives)})." + ) + timer.start('prepare_matrices') inf = float('inf') ninf = -inf From 4ce69351e41e3dac0fa4a9f63f03c9e51805996a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:43:03 -0600 Subject: [PATCH 0886/1178] NFC: adding docstrings --- .../cp/scheduling_expr/scheduling_logic.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index 98e0c1ceabd..e5695b57c5c 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -29,15 +29,26 @@ def _to_string(self, values, verbose, smap): class AlternativeExpression(NaryBooleanExpression): """ - TODO/ + Expression over IntervalVars representing that if the first arg is present, + then exactly one of the following args must be present. The first arg is + absent if and only if all the others are absent. """ + # [ESJ 4/4/24]: docplex takes an optional 'cardinality' argument with this + # too--it generalized to "exactly n" of the intervals have to exist, + # basically. It would be nice to include this eventually, but this is + # probably fine for now. + def _to_string(self, values, verbose, smap): return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) class SynchronizeExpression(NaryBooleanExpression): - """ """ + """ + Expression over IntervalVars synchronizing the first argument with all of the + following arguments. That is, if the first argument is present, the remaining + arguments start and end at the same time as it. + """ def _to_string(self, values, verbose, smap): return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) From 6fa6196a3e66c42b91a19e69ef9275c6289c493c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:48:34 -0600 Subject: [PATCH 0887/1178] standard_form: add option to control the final optimization sense --- pyomo/contrib/solver/gurobi_direct.py | 5 ++++- pyomo/repn/plugins/standard_form.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index e0e0d7d32b0..36a783cba02 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -17,6 +17,7 @@ from pyomo.common.config import ConfigValue from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import attempt_import +from pyomo.common.enums import OptimizationSense from pyomo.common.shutdown import python_is_shutting_down from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer @@ -222,7 +223,9 @@ def solve(self, model, **kwds) -> Results: StaleFlagManager.mark_all_as_stale() timer.start('compile_model') - repn = LinearStandardFormCompiler().write(model, mixed_form=True) + repn = LinearStandardFormCompiler().write( + model, mixed_form=True, set_sense=None + ) timer.stop('compile_model') if len(repn.objectives) > 1: diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 0211ba44387..566d0d8d932 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -20,6 +20,7 @@ document_kwargs_from_configdict, ) from pyomo.common.dependencies import scipy, numpy as np +from pyomo.common.enums import OptimizationSense from pyomo.common.gc_manager import PauseGC from pyomo.common.timing import TicTocTimer @@ -158,6 +159,14 @@ class LinearStandardFormCompiler(object): 'mix of <=, ==, and >=)', ), ) + CONFIG.declare( + 'set_sense', + ConfigValue( + default=OptimizationSense.minimize, + domain=InEnum(OptimizationSense), + description='If not None, map all objectives to the specified sense.', + ), + ) CONFIG.declare( 'show_section_timing', ConfigValue( @@ -315,6 +324,7 @@ def write(self, model): # # Process objective # + set_sense = self.config.set_sense objectives = [] for blk in component_map[Objective]: objectives.extend( @@ -336,7 +346,7 @@ def write(self, model): N = len(repn.linear) obj_data.append(np.fromiter(repn.linear.values(), float, N)) obj_offset.append(repn.constant) - if obj.sense == maximize: + if set_sense is not None and set_sense != obj.sense: obj_data[-1] *= -1 obj_offset[-1] *= -1 obj_index.append( From d546a248493a34623e61e46f002f3d85b8ef2b66 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:49:00 -0600 Subject: [PATCH 0888/1178] Add gurobi_direcct to the solver test suite --- pyomo/contrib/solver/tests/solvers/test_solvers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index a4f4a3bc389..f91de2287b7 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -21,6 +21,7 @@ from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.ipopt import Ipopt from pyomo.contrib.solver.gurobi import Gurobi +from pyomo.contrib.solver.gurobi_direct import GurobiDirect from pyomo.core.expr.numeric_expr import LinearExpression @@ -32,8 +33,8 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] -mip_solvers = [('gurobi', Gurobi)] +all_solvers = [('gurobi', Gurobi), ('gurobi_direct', GurobiDirect), ('ipopt', Ipopt)] +mip_solvers = [('gurobi', Gurobi), ('gurobi_direct', GurobiDirect)] nlp_solvers = [('ipopt', Ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] miqcqp_solvers = [('gurobi', Gurobi)] From a234fd42f44ef0acd4d789acfae05cbff8dcec70 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:49:59 -0600 Subject: [PATCH 0889/1178] Taking the debugging util stuff out of this PR--I'll think about how to do it better later --- pyomo/contrib/cp/debugging.py | 29 ------------------------ pyomo/contrib/cp/tests/test_debugging.py | 25 -------------------- 2 files changed, 54 deletions(-) delete mode 100644 pyomo/contrib/cp/debugging.py delete mode 100644 pyomo/contrib/cp/tests/test_debugging.py diff --git a/pyomo/contrib/cp/debugging.py b/pyomo/contrib/cp/debugging.py deleted file mode 100644 index 34fb105a571..00000000000 --- a/pyomo/contrib/cp/debugging.py +++ /dev/null @@ -1,29 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.opt import WriterFactory - - -def write_conflict_set(m, filename): - """ - For debugging infeasible CPs: writes the conflict set found by CP optimizer - to a file with the specified filename. - - Args: - m: Pyomo CP model - filename: string filename - """ - - cpx_mod, var_map = WriterFactory('docplex_model').write( - m, symbolic_solver_labels=True - ) - conflict = cpx_mod.refine_conflict() - conflict.write(filename) diff --git a/pyomo/contrib/cp/tests/test_debugging.py b/pyomo/contrib/cp/tests/test_debugging.py deleted file mode 100644 index 8e24e545724..00000000000 --- a/pyomo/contrib/cp/tests/test_debugging.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.common.unittest as unittest - -from pyomo.environ import ConcreteModel, Constraint, Var - - -class TestCPDebugging(unittest.TestCase): - def test_debug_infeasibility(self): - m = ConcreteModel() - m.x = Var(domain=Integers, bounds=(2, 5)) - m.y = Var(domain=Integers, bounds=(7, 12)) - m.c = Constraint(expr=m.y <= m.x) - - # ESJ TODO: I don't know how to do this without a baseline, which we - # really don't want... From 30490d24bcdf58763330f3216cbc30c93e559089 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:58:14 -0600 Subject: [PATCH 0890/1178] Correct import of ObjectiveSense enum --- pyomo/contrib/solver/gurobi_direct.py | 4 ++-- pyomo/repn/plugins/standard_form.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 36a783cba02..7b80651ccae 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -17,7 +17,7 @@ from pyomo.common.config import ConfigValue from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import attempt_import -from pyomo.common.enums import OptimizationSense +from pyomo.common.enums import ObjectiveSense from pyomo.common.shutdown import python_is_shutting_down from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer @@ -371,7 +371,7 @@ def _postsolve(self, timer: HierarchicalTimer, config, loader): try: results.objective_bound = grb_model.ObjBound except (gurobipy.GurobiError, AttributeError): - if grb_model.ModelSense == OptimizationSense.minimize: + if grb_model.ModelSense == ObjectiveSense.minimize: results.objective_bound = -math.inf else: results.objective_bound = math.inf diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 566d0d8d932..a5aaece8531 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -20,7 +20,7 @@ document_kwargs_from_configdict, ) from pyomo.common.dependencies import scipy, numpy as np -from pyomo.common.enums import OptimizationSense +from pyomo.common.enums import ObjectiveSense from pyomo.common.gc_manager import PauseGC from pyomo.common.timing import TicTocTimer @@ -162,8 +162,8 @@ class LinearStandardFormCompiler(object): CONFIG.declare( 'set_sense', ConfigValue( - default=OptimizationSense.minimize, - domain=InEnum(OptimizationSense), + default=ObjectiveSense.minimize, + domain=InEnum(ObjectiveSense), description='If not None, map all objectives to the specified sense.', ), ) From b57adf1bc3aaa5c9c93e1988e31da94eb6ad81d7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 13:17:29 -0600 Subject: [PATCH 0891/1178] Add gurobi_direct to the docs --- doc/OnlineDocs/developer_reference/solvers.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 94fb684236f..9e3281246f4 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -45,9 +45,12 @@ with existing interfaces). * - Ipopt - ``ipopt`` - ``ipopt_v2`` - * - Gurobi + * - Gurobi (persistent) - ``gurobi`` - ``gurobi_v2`` + * - Gurobi (direct) + - ``gurobi_direct`` + - ``gurobi_direct_v2`` Using the new interfaces through the legacy interface ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 942df952e5c79e9ddffe53a2fe98c36fce96132b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 4 Apr 2024 13:40:45 -0600 Subject: [PATCH 0892/1178] NFC: Typo fix in amplfunc_merge.py --- pyomo/solvers/amplfunc_merge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 9d127a94396..e49fd20e20f 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -28,5 +28,5 @@ def amplfunc_string_merge(amplfunc, pyomo_amplfunc): def amplfunc_merge(env): - """Merge AMPLFUNC and PYOMO_AMPLFuNC in an environment var dict""" + """Merge AMPLFUNC and PYOMO_AMPLFUNC in an environment var dict""" return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) From 9790e080a7f3412841125e7c38ada2506e583ea2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 13:55:08 -0600 Subject: [PATCH 0893/1178] NFC: fix typo --- pyomo/repn/plugins/standard_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index a5aaece8531..110e95c3c6d 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -96,7 +96,7 @@ class LinearStandardFormInfo(object): objectives : List[_ObjectiveData] - The list of Pyomo objective objects correcponding to the active objectives + The list of Pyomo objective objects corresponding to the active objectives eliminated_vars: List[Tuple[_VarData, NumericExpression]] From 5dac102e6391311ff7a5615b870199dc3776842a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 17:13:34 -0600 Subject: [PATCH 0894/1178] Adding test requested by PR review --- pyomo/common/tests/test_enums.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py index 52ee1c5abb3..80d081505e9 100644 --- a/pyomo/common/tests/test_enums.py +++ b/pyomo/common/tests/test_enums.py @@ -59,6 +59,8 @@ def test_call(self): with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"): ProblemSense('foo') + with self.assertRaisesRegex(ValueError, "2 is not a valid ProblemSense"): + ProblemSense(2) def test_contains(self): self.assertIn(ProblemSense.unknown, ProblemSense) From 7b46ea5f2c6fecd38a260ccd56968493acb70911 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 5 Apr 2024 08:09:05 -0400 Subject: [PATCH 0895/1178] Make `symbolic_solver_labels` configurable --- pyomo/contrib/pyros/config.py | 15 +++++++++++++++ pyomo/contrib/pyros/tests/test_grcs.py | 2 ++ pyomo/contrib/pyros/util.py | 7 +++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index bc2bfd591e6..8ab24939349 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -503,6 +503,21 @@ def pyros_config(): ), ), ) + CONFIG.declare( + 'symbolic_solver_labels', + ConfigValue( + default=False, + domain=bool, + description=( + """ + True to ensure the component names given to the + subordinate solvers for every subproblem reflect + the names of the corresponding Pyomo modeling components, + False otherwise. + """ + ), + ), + ) # ================================================ # === Required User Inputs diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 41223b30899..d49ed6b1002 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3795,6 +3795,7 @@ def test_solve_master(self): config.declare( "progress_logger", ConfigValue(default=logging.getLogger(__name__)) ) + config.declare("symbolic_solver_labels", ConfigValue(default=False)) with time_code(master_data.timing, "main", is_main_timer=True): master_soln = solve_master(master_data, config) @@ -6171,6 +6172,7 @@ def test_log_config(self): " keepfiles=False\n" " tee=False\n" " load_solution=True\n" + " symbolic_solver_labels=False\n" " objective_focus=\n" " nominal_uncertain_param_vals=[0.5]\n" " decision_rule_order=0\n" diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 5d386240609..23cde45d0cf 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1799,7 +1799,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): If ApplicationError is raised by the solver. In this case, `err_msg` is logged through ``config.progress_logger.exception()`` before - the excception is raised. + the exception is raised. """ tt_timer = TicTocTimer() @@ -1811,7 +1811,10 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): try: results = solver.solve( - model, tee=config.tee, load_solutions=False, symbolic_solver_labels=True + model, + tee=config.tee, + load_solutions=False, + symbolic_solver_labels=config.symbolic_solver_labels, ) except ApplicationError: # account for possible external subsolver errors From f766b33b2f7cea168a1a1b1ba49d49b8b7e4f546 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 5 Apr 2024 08:10:55 -0400 Subject: [PATCH 0896/1178] Make log example reflective of `symbolic_solver_labels` --- doc/OnlineDocs/contributed_packages/pyros.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 9faa6d1365f..95049eded8a 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -926,6 +926,7 @@ Observe that the log contains the following information: keepfiles=False tee=False load_solution=True + symbolic_solver_labels=False objective_focus= nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] decision_rule_order=1 From b0217c86ec3baf36126cdcb8371b1e4dbf7e3874 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 15:18:56 -0600 Subject: [PATCH 0897/1178] Moving exit node dispatcher onto base class from pyomo.repn.util and fixing the monomial expression tests --- pyomo/contrib/cp/repn/docplex_writer.py | 20 ++++++++++++++----- pyomo/contrib/cp/tests/test_docplex_walker.py | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 98d3e07e8ed..5dd355dc70d 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -88,6 +88,7 @@ from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet from pyomo.core.base.set import SetProduct +from pyomo.repn.util import ExitNodeDispatcher from pyomo.opt import WriterFactory, SolverFactory, TerminationCondition, SolverResults ### FIXME: Remove the following as soon as non-active components no @@ -707,9 +708,11 @@ def _get_bool_valued_expr(arg): def _handle_monomial_expr(visitor, node, arg1, arg2): # Monomial terms show up a lot. This handles some common # simplifications (necessary in part for the unit tests) + print(arg1) + print(arg2) if arg2[1].__class__ in EXPR.native_types: return _GENERAL, arg1[1] * arg2[1] - elif arg1[1] == 1: + elif arg1[1].__class__ in EXPR.native_types and arg1[1] == 1: return arg2 return (_GENERAL, cp.times(_get_int_valued_expr(arg1), _get_int_valued_expr(arg2))) @@ -997,8 +1000,7 @@ def _handle_synchronize_expression_node(visitor, node, *args): return _GENERAL, cp.synchronize(args[0][1], [arg[1] for arg in args[1:]]) -class LogicalToDoCplex(StreamBasedExpressionVisitor): - _operator_handles = { +_operator_handles = { EXPR.GetItemExpression: _handle_getitem, EXPR.Structural_GetItemExpression: _handle_getitem, EXPR.Numeric_GetItemExpression: _handle_getitem, @@ -1047,6 +1049,14 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): AlternativeExpression: _handle_alternative_expression_node, SynchronizeExpression: _handle_synchronize_expression_node, } + + +class LogicalToDoCplex(StreamBasedExpressionVisitor): + exit_node_dispatcher = ExitNodeDispatcher( + _operator_handles + ) + # NOTE: Because of indirection, we can encounter indexed Params and Vars in + # expressions _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, IntervalVarEndTime: _before_interval_var_end_time, @@ -1065,7 +1075,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IndexedBooleanVar: _before_indexed_boolean_var, _GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, - IndexedParam: _before_indexed_param, # Because of indirection + IndexedParam: _before_indexed_param, ScalarParam: _before_param, _ParamData: _before_param, } @@ -1101,7 +1111,7 @@ def beforeChild(self, node, child, child_idx): return True, None def exitNode(self, node, data): - return self._operator_handles[node.__class__](self, node, *data) + return self.exit_node_dispatcher[node.__class__](self, node, *data) finalizeResult = None diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index d14e0bc2d6f..a7e537ee15b 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -261,11 +261,11 @@ def test_monomial_expressions(self): e = (1 / m.p) * m.x expr = visitor.walk_expression((e, e, 0)) - self.assertTrue(expr[1].equals(0.25 * x)) + self.assertTrue(expr[1].equals(cp.float_div(1, 4) * x)) e = (m.p ** (0.5)) * m.x expr = visitor.walk_expression((e, e, 0)) - self.assertTrue(expr[1].equals(2 * x)) + self.assertTrue(expr[1].equals(cp.power(4, 0.5) * x)) @unittest.skipIf(not docplex_available, "docplex is not available") From 5424f604281bcd1f09ae16d710e55509a9de368e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 15:22:35 -0600 Subject: [PATCH 0898/1178] Removing some redundant operator handlers now that I have the subclass magic for automatic registration. --- pyomo/contrib/cp/repn/docplex_writer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 5dd355dc70d..50f5f9e4294 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1002,13 +1002,7 @@ def _handle_synchronize_expression_node(visitor, node, *args): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, - EXPR.Structural_GetItemExpression: _handle_getitem, - EXPR.Numeric_GetItemExpression: _handle_getitem, - EXPR.Boolean_GetItemExpression: _handle_getitem, EXPR.GetAttrExpression: _handle_getattr, - EXPR.Structural_GetAttrExpression: _handle_getattr, - EXPR.Numeric_GetAttrExpression: _handle_getattr, - EXPR.Boolean_GetAttrExpression: _handle_getattr, EXPR.CallExpression: _handle_call, EXPR.NegationExpression: _handle_negation_node, EXPR.ProductExpression: _handle_product_node, @@ -1017,7 +1011,6 @@ def _handle_synchronize_expression_node(visitor, node, *args): EXPR.AbsExpression: _handle_abs_node, EXPR.MonomialTermExpression: _handle_monomial_expr, EXPR.SumExpression: _handle_sum_node, - EXPR.LinearExpression: _handle_sum_node, EXPR.MinExpression: _handle_min_node, EXPR.MaxExpression: _handle_max_node, EXPR.NotExpression: _handle_not_node, From e6f0a10f64c802eb7f2a2fcb24d8bb9bf41a1f2d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 16:58:35 -0600 Subject: [PATCH 0899/1178] black --- pyomo/contrib/cp/repn/docplex_writer.py | 86 ++++++++++++------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 50f5f9e4294..0263b6b82a1 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1001,53 +1001,51 @@ def _handle_synchronize_expression_node(visitor, node, *args): _operator_handles = { - EXPR.GetItemExpression: _handle_getitem, - EXPR.GetAttrExpression: _handle_getattr, - EXPR.CallExpression: _handle_call, - EXPR.NegationExpression: _handle_negation_node, - EXPR.ProductExpression: _handle_product_node, - EXPR.DivisionExpression: _handle_division_node, - EXPR.PowExpression: _handle_pow_node, - EXPR.AbsExpression: _handle_abs_node, - EXPR.MonomialTermExpression: _handle_monomial_expr, - EXPR.SumExpression: _handle_sum_node, - EXPR.MinExpression: _handle_min_node, - EXPR.MaxExpression: _handle_max_node, - EXPR.NotExpression: _handle_not_node, - EXPR.EquivalenceExpression: _handle_equivalence_node, - EXPR.ImplicationExpression: _handle_implication_node, - EXPR.AndExpression: _handle_and_node, - EXPR.OrExpression: _handle_or_node, - EXPR.XorExpression: _handle_xor_node, - EXPR.ExactlyExpression: _handle_exactly_node, - EXPR.AtMostExpression: _handle_at_most_node, - EXPR.AtLeastExpression: _handle_at_least_node, - EXPR.AllDifferentExpression: _handle_all_diff_node, - EXPR.CountIfExpression: _handle_count_if_node, - EXPR.EqualityExpression: _handle_equality_node, - EXPR.NotEqualExpression: _handle_not_equal_node, - EXPR.InequalityExpression: _handle_inequality_node, - EXPR.RangedExpression: _handle_ranged_inequality_node, - BeforeExpression: _handle_before_expression_node, - AtExpression: _handle_at_expression_node, - AlwaysIn: _handle_always_in_node, - _GeneralExpressionData: _handle_named_expression_node, - ScalarExpression: _handle_named_expression_node, - NoOverlapExpression: _handle_no_overlap_expression_node, - FirstInSequenceExpression: _handle_first_in_sequence_expression_node, - LastInSequenceExpression: _handle_last_in_sequence_expression_node, - BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, - PredecessorToExpression: _handle_predecessor_to_expression_node, - SpanExpression: _handle_span_expression_node, - AlternativeExpression: _handle_alternative_expression_node, - SynchronizeExpression: _handle_synchronize_expression_node, - } + EXPR.GetItemExpression: _handle_getitem, + EXPR.GetAttrExpression: _handle_getattr, + EXPR.CallExpression: _handle_call, + EXPR.NegationExpression: _handle_negation_node, + EXPR.ProductExpression: _handle_product_node, + EXPR.DivisionExpression: _handle_division_node, + EXPR.PowExpression: _handle_pow_node, + EXPR.AbsExpression: _handle_abs_node, + EXPR.MonomialTermExpression: _handle_monomial_expr, + EXPR.SumExpression: _handle_sum_node, + EXPR.MinExpression: _handle_min_node, + EXPR.MaxExpression: _handle_max_node, + EXPR.NotExpression: _handle_not_node, + EXPR.EquivalenceExpression: _handle_equivalence_node, + EXPR.ImplicationExpression: _handle_implication_node, + EXPR.AndExpression: _handle_and_node, + EXPR.OrExpression: _handle_or_node, + EXPR.XorExpression: _handle_xor_node, + EXPR.ExactlyExpression: _handle_exactly_node, + EXPR.AtMostExpression: _handle_at_most_node, + EXPR.AtLeastExpression: _handle_at_least_node, + EXPR.AllDifferentExpression: _handle_all_diff_node, + EXPR.CountIfExpression: _handle_count_if_node, + EXPR.EqualityExpression: _handle_equality_node, + EXPR.NotEqualExpression: _handle_not_equal_node, + EXPR.InequalityExpression: _handle_inequality_node, + EXPR.RangedExpression: _handle_ranged_inequality_node, + BeforeExpression: _handle_before_expression_node, + AtExpression: _handle_at_expression_node, + AlwaysIn: _handle_always_in_node, + _GeneralExpressionData: _handle_named_expression_node, + ScalarExpression: _handle_named_expression_node, + NoOverlapExpression: _handle_no_overlap_expression_node, + FirstInSequenceExpression: _handle_first_in_sequence_expression_node, + LastInSequenceExpression: _handle_last_in_sequence_expression_node, + BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, + PredecessorToExpression: _handle_predecessor_to_expression_node, + SpanExpression: _handle_span_expression_node, + AlternativeExpression: _handle_alternative_expression_node, + SynchronizeExpression: _handle_synchronize_expression_node, +} class LogicalToDoCplex(StreamBasedExpressionVisitor): - exit_node_dispatcher = ExitNodeDispatcher( - _operator_handles - ) + exit_node_dispatcher = ExitNodeDispatcher(_operator_handles) # NOTE: Because of indirection, we can encounter indexed Params and Vars in # expressions _var_handles = { From 558df7097a4ef5ee757847b8099bd15b8db8ac5d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 17:17:34 -0600 Subject: [PATCH 0900/1178] Removing a lot of unused imports --- pyomo/contrib/cp/repn/docplex_writer.py | 3 +-- .../contrib/cp/scheduling_expr/step_function_expressions.py | 1 - pyomo/contrib/cp/tests/test_docplex_walker.py | 5 ----- pyomo/contrib/cp/tests/test_sequence_expressions.py | 5 ++--- pyomo/contrib/cp/tests/test_sequence_var.py | 2 +- pyomo/contrib/cp/transform/logical_to_disjunctive_program.py | 1 - pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py | 4 ---- 7 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 0263b6b82a1..c48d5858b0e 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -33,7 +33,6 @@ from pyomo.contrib.cp.sequence_var import ( SequenceVar, ScalarSequenceVar, - IndexedSequenceVar, _SequenceVarData, ) from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( @@ -81,7 +80,7 @@ _GeneralBooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar import pyomo.core.expr as EXPR diff --git a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py index b75306f72c9..129dff66b48 100644 --- a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py @@ -15,7 +15,6 @@ IntervalVarStartTime, IntervalVarEndTime, ) -from pyomo.core.base.component import Component from pyomo.core.expr.base import ExpressionBase from pyomo.core.expr.logical_expr import BooleanExpression diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index a7e537ee15b..9aa91b5185f 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -17,13 +17,10 @@ no_overlap, first_in_sequence, last_in_sequence, - before_in_sequence, - predecessor_to, alternative, synchronize, ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( - AlwaysIn, Step, Pulse, ) @@ -56,8 +53,6 @@ Integers, inequality, Expression, - Reals, - Set, Param, ) diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index b676881e379..c7cf94f23d5 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from io import StringIO import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( @@ -32,8 +31,8 @@ first_in_sequence, last_in_sequence, ) -from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar -from pyomo.environ import ConcreteModel, Integers, LogicalConstraint, Set, value, Var +from pyomo.contrib.cp.sequence_var import SequenceVar +from pyomo.environ import ConcreteModel, LogicalConstraint, Set class TestSequenceVarExpressions(unittest.TestCase): diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index ebff465a376..c1e205c6326 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -13,7 +13,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar -from pyomo.environ import ConcreteModel, Integers, Set, value, Var +from pyomo.environ import ConcreteModel, Set class TestScalarSequenceVar(unittest.TestCase): diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py index e318e621e88..3c6960bc198 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py @@ -12,7 +12,6 @@ from pyomo.contrib.cp.transform.logical_to_disjunctive_walker import ( LogicalToDisjunctiveVisitor, ) -from pyomo.common.collections import ComponentMap from pyomo.common.modeling import unique_component_name from pyomo.common.config import ConfigDict, ConfigValue diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index d5f13e91535..09bba403850 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -9,14 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import collections - from pyomo.common.collections import ComponentMap from pyomo.common.errors import MouseTrap from pyomo.core.expr.expr_common import ExpressionType from pyomo.core.expr.visitor import StreamBasedExpressionVisitor -from pyomo.core.expr.numeric_expr import NumericExpression -from pyomo.core.expr.relational_expr import RelationalExpression import pyomo.core.expr as EXPR from pyomo.core.base import ( Binary, From a4a78ed5c629ab3f4b953f591537b10450b0a977 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 17:17:59 -0600 Subject: [PATCH 0901/1178] black --- pyomo/contrib/cp/tests/test_docplex_walker.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 9aa91b5185f..1173ae66eab 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -20,10 +20,7 @@ alternative, synchronize, ) -from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( - Step, - Pulse, -) +from pyomo.contrib.cp.scheduling_expr.step_function_expressions import Step, Pulse from pyomo.contrib.cp.repn.docplex_writer import docplex_available, LogicalToDoCplex from pyomo.core.base.range import NumericRange From 3e09bd2f5ca5bec451ba4e07d3886615173c1b9b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 17:19:16 -0600 Subject: [PATCH 0902/1178] removing debugging --- pyomo/contrib/cp/repn/docplex_writer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index c48d5858b0e..c8a1143a7b5 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -707,8 +707,6 @@ def _get_bool_valued_expr(arg): def _handle_monomial_expr(visitor, node, arg1, arg2): # Monomial terms show up a lot. This handles some common # simplifications (necessary in part for the unit tests) - print(arg1) - print(arg2) if arg2[1].__class__ in EXPR.native_types: return _GENERAL, arg1[1] * arg2[1] elif arg1[1].__class__ in EXPR.native_types and arg1[1] == 1: From ad7011f12e352ca25212bd845893b3c2bb978318 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 8 Apr 2024 12:09:58 -0600 Subject: [PATCH 0903/1178] check _skip_trivial_costraints before the constraint body --- pyomo/solvers/plugins/solvers/gurobi_direct.py | 5 ++--- pyomo/solvers/plugins/solvers/xpress_direct.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 1d88eced629..ed66a4e0e7b 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -493,9 +493,8 @@ def _add_constraint(self, con): if not con.active: return None - if is_fixed(con.body): - if self._skip_trivial_constraints: - return None + if self._skip_trivial_constraints and is_fixed(con.body): + return None conname = self._symbol_map.getSymbol(con, self._labeler) diff --git a/pyomo/solvers/plugins/solvers/xpress_direct.py b/pyomo/solvers/plugins/solvers/xpress_direct.py index 75cf8f921df..c62f76d85ce 100644 --- a/pyomo/solvers/plugins/solvers/xpress_direct.py +++ b/pyomo/solvers/plugins/solvers/xpress_direct.py @@ -667,9 +667,8 @@ def _add_constraint(self, con): if not con.active: return None - if is_fixed(con.body): - if self._skip_trivial_constraints: - return None + if self._skip_trivial_constraints and is_fixed(con.body): + return None conname = self._symbol_map.getSymbol(con, self._labeler) From 9bf65310dc34fd555918dcc39a58b57349156a4d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 9 Apr 2024 15:01:53 -0600 Subject: [PATCH 0904/1178] Bug fixes: name and solutions attributes --- pyomo/contrib/solver/base.py | 11 +++++++---- pyomo/contrib/solver/factory.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index cec392271f6..69f32b45075 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -61,10 +61,7 @@ def __init__(self, **kwds) -> None: # We allow the user and/or developer to name the solver something else, # if they really desire. Otherwise it defaults to the class name (all lowercase) if "name" in kwds: - self.name = kwds["name"] - kwds.pop('name') - else: - self.name = type(self).__name__.lower() + self.name = kwds.pop('name') self.config = self.CONFIG(value=kwds) # @@ -499,6 +496,12 @@ def _solution_handler( """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') + if not hasattr(model, 'solutions'): + # This logic gets around Issue #2130 in which + # solutions is not an attribute on Blocks + from pyomo.core.base.PyomoModel import ModelSolutions + + setattr(model, 'solutions', ModelSolutions(model)) model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 99fbcc3a6d0..71bf81ee15b 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -31,6 +31,7 @@ class LegacySolver(LegacySolverWrapper, cls): LegacySolver ) + cls.name = name return cls return decorator From 7268a954a83bfe83ee01620a9d16ddcfba9082ab Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 9 Apr 2024 15:05:51 -0600 Subject: [PATCH 0905/1178] Add back in logic for if there is no name attr --- pyomo/contrib/solver/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 69f32b45075..8d49344fbcf 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,8 +14,8 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import Constraint, _GeneralConstraintData -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import Objective, _GeneralObjectiveData @@ -62,6 +62,8 @@ def __init__(self, **kwds) -> None: # if they really desire. Otherwise it defaults to the class name (all lowercase) if "name" in kwds: self.name = kwds.pop('name') + elif not hasattr(self, 'name'): + self.name = type(self).__name__.lower() self.config = self.CONFIG(value=kwds) # From 9b4fd6f4f2a2a5f3b959cb9397d7923498280b1d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 9 Apr 2024 15:12:27 -0600 Subject: [PATCH 0906/1178] Add relevant comments --- pyomo/contrib/solver/base.py | 5 ++++- pyomo/contrib/solver/factory.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8d49344fbcf..736bf8b5a7c 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -59,7 +59,10 @@ class SolverBase(abc.ABC): def __init__(self, **kwds) -> None: # We allow the user and/or developer to name the solver something else, - # if they really desire. Otherwise it defaults to the class name (all lowercase) + # if they really desire. + # Otherwise it defaults to the name defined when the solver was registered + # in the SolverFactory or the class name (all lowercase), whichever is + # applicable if "name" in kwds: self.name = kwds.pop('name') elif not hasattr(self, 'name'): diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 71bf81ee15b..d3ca1329af3 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -31,6 +31,7 @@ class LegacySolver(LegacySolverWrapper, cls): LegacySolver ) + # Preserve the preferred name, as registered in the Factory cls.name = name return cls From 88535d42141f0aa678ec88b8a86cf23684faf9aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 10 Apr 2024 09:24:12 -0600 Subject: [PATCH 0907/1178] Remove remaining references to '_.*Data' classes --- pyomo/contrib/pyros/config.py | 2 +- pyomo/core/base/block.py | 18 +++++++++--------- pyomo/gdp/util.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 59bf9a9ab37..31e462223da 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -98,7 +98,7 @@ class InputDataStandardizer(object): Pyomo component type, such as Component, Var or Param. cdatatype : type Corresponding Pyomo component data type, such as - _ComponentData, VarData, or ParamData. + ComponentData, VarData, or ParamData. ctype_validator : callable, optional Validator function for objects of type `ctype`. cdatatype_validator : callable, optional diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 8f4e86fe697..3eb18dde7a9 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -160,13 +160,13 @@ def __init__(self): self.seen_data = set() def unique(self, comp, items, are_values): - """Returns generator that filters duplicate _ComponentData objects from items + """Returns generator that filters duplicate ComponentData objects from items Parameters ---------- comp: ComponentBase The Component (indexed or scalar) that contains all - _ComponentData returned by the `items` generator. `comp` may + ComponentData returned by the `items` generator. `comp` may be an IndexedComponent generated by :py:func:`Reference` (and hence may not own the component datas in `items`) @@ -175,8 +175,8 @@ def unique(self, comp, items, are_values): `comp` Component. are_values: bool - If `True`, `items` yields _ComponentData objects, otherwise, - `items` yields `(index, _ComponentData)` tuples. + If `True`, `items` yields ComponentData objects, otherwise, + `items` yields `(index, ComponentData)` tuples. """ if comp.is_reference(): @@ -1399,7 +1399,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): Generator that returns a nested 2-tuple of - ((component name, index value), _ComponentData) + ((component name, index value), ComponentData) for every component data in the block matching the specified ctype(s). @@ -1416,7 +1416,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): Iterate over the components in a specified sorted order dedup: _DeduplicateInfo - Deduplicator to prevent returning the same _ComponentData twice + Deduplicator to prevent returning the same ComponentData twice """ for name, comp in PseudoMap(self, ctype, active, sort).items(): # NOTE: Suffix has a dict interface (something other derived @@ -1452,7 +1452,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): yield from dedup.unique(comp, _items, False) def _component_data_itervalues(self, ctype, active, sort, dedup): - """Generator that returns the _ComponentData for every component data + """Generator that returns the ComponentData for every component data in the block. Parameters @@ -1467,7 +1467,7 @@ def _component_data_itervalues(self, ctype, active, sort, dedup): Iterate over the components in a specified sorted order dedup: _DeduplicateInfo - Deduplicator to prevent returning the same _ComponentData twice + Deduplicator to prevent returning the same ComponentData twice """ for comp in PseudoMap(self, ctype, active, sort).values(): # NOTE: Suffix has a dict interface (something other derived @@ -1573,7 +1573,7 @@ def component_data_iterindex( generator recursively descends into sub-blocks. The tuple is - ((component name, index value), _ComponentData) + ((component name, index value), ComponentData) """ dedup = _DeduplicateInfo() diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index ee905791c26..2fe8e9e1dee 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -534,7 +534,7 @@ def get_transformed_constraints(srcConstraint): "want the container for all transformed constraints " "from an IndexedDisjunction, this is the parent " "component of a transformed constraint originating " - "from any of its _ComponentDatas.)" + "from any of its ComponentDatas.)" ) transBlock = _get_constraint_transBlock(srcConstraint) transformed_constraints = transBlock.private_data( From b235295174500eacb3bc0c2171d298b3ce3b8d48 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 10 Apr 2024 09:31:02 -0600 Subject: [PATCH 0908/1178] calculate_variable_from_constraint: add check for indexed constraint --- pyomo/util/calc_var_value.py | 7 +++++++ pyomo/util/tests/test_calc_var_value.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index 156ad56dffb..42ee3119361 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -85,6 +85,13 @@ def calculate_variable_from_constraint( constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() + if constraint.is_indexed(): + raise ValueError( + 'calculate_variable_from_constraint(): constraint must be a ' + 'scalar constraint or a single ConstraintData. Received ' + f'{constraint.__class__.__name__} ("{constraint.name}")' + ) + body = constraint.body lower = constraint.lb upper = constraint.ub diff --git a/pyomo/util/tests/test_calc_var_value.py b/pyomo/util/tests/test_calc_var_value.py index a02d7a7d838..4bed4d5c843 100644 --- a/pyomo/util/tests/test_calc_var_value.py +++ b/pyomo/util/tests/test_calc_var_value.py @@ -101,6 +101,15 @@ def test_initialize_value(self): ): calculate_variable_from_constraint(m.x, m.lt) + m.indexed = Constraint([1, 2], rule=lambda m, i: m.x <= i) + with self.assertRaisesRegex( + ValueError, + r"calculate_variable_from_constraint\(\): constraint must be a scalar " + r"constraint or a single ConstraintData. Received IndexedConstraint " + r'\("indexed"\)', + ): + calculate_variable_from_constraint(m.x, m.indexed) + def test_linear(self): m = ConcreteModel() m.x = Var() From 76cd7469b5faeb970ccf5b62e7f7e85e6c96cabd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 10 Apr 2024 09:44:42 -0600 Subject: [PATCH 0909/1178] NFC: Update docstring to fix typo and copy/paste errors --- pyomo/core/base/boolean_var.py | 44 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 98761dee536..67c06bdacce 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -81,26 +81,30 @@ def _associated_binary_mapper(encode, val): class BooleanVarData(ComponentData, BooleanValue): - """ - This class defines the data for a single Boolean variable. - - Constructor Arguments: - component The BooleanVar object that owns this data. - - Public Class Attributes: - domain The domain of this variable. - fixed If True, then this variable is treated as a - fixed constant in the model. - stale A Boolean indicating whether the value of this variable is - legitimiate. This value is true if the value should - be considered legitimate for purposes of reporting or - other interrogation. - value The numeric value of this variable. - - The domain attribute is a property because it is - too widely accessed directly to enforce explicit getter/setter - methods and we need to deter directly modifying or accessing - these attributes in certain cases. + """This class defines the data for a single Boolean variable. + + Parameters + ---------- + component: Component + The BooleanVar object that owns this data. + + Attributes + ---------- + domain: SetData + The domain of this variable. + + fixed: bool + If True, then this variable is treated as a fixed constant in + the model. + + stale: bool + A Boolean indicating whether the value of this variable is + Consistent with the most recent solve. `True` indicates that + this variable's value was set prior to the most recent solve and + was not updated by the results returned by the solve. + + value: bool + The value of this variable. """ __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') From d19c8c9b5d9c5aeb3e04ee55d543add5e75a7cf7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 11 Apr 2024 08:30:41 -0600 Subject: [PATCH 0910/1178] Disable the use of universal newlines in the ipopt_v2 NL file --- pyomo/contrib/solver/ipopt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5f601b7a9f7..588e06ad74c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -307,7 +307,12 @@ def solve(self, model, **kwds): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - with open(basename + '.nl', 'w') as nl_file, open( + # Note: the ASL has an issue where string constants written + # to the NL file (e.g. arguments in external functions) MUST + # be terminated with '\n' regardless of platform. We will + # disable universal newlines in the NL file to prevent + # Python from mapping those '\n' to '\r\n' on Windows. + with open(basename + '.nl', 'w', newline='\n') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: timer.start('write_nl_file') From b63358c5eb6578102917d07e00380fa3188973e3 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 16 Apr 2024 14:57:55 -0400 Subject: [PATCH 0911/1178] add highs version requirements for pr workflow --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a2060240391..28c72541a13 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -606,7 +606,7 @@ jobs: shell: bash run: | echo "NOTE: temporarily pinning to highspy pre-release for testing" - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy==1.7.1.dev1 \ + $PYTHON_EXE -m pip install --cache-dir cache/pip highspy>=1.7.1.dev1 \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking From 747da89e8124d04955ebc161a74c227f658289e6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Apr 2024 14:43:14 -0600 Subject: [PATCH 0912/1178] TEMPORARY FIX: Pin to mpi4py 3.1.5 --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 55f903a37f9..a15240194f2 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -92,7 +92,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py + PACKAGES: mpi4py==3.1.5 - os: ubuntu-latest python: '3.10' diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..2615f6b838e 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -93,7 +93,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py + PACKAGES: mpi4py==3.1.5 - os: ubuntu-latest python: '3.11' From 28f5080a4b3506200380d89bbe2cfedf9e23a282 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:09:07 -0600 Subject: [PATCH 0913/1178] nlv2: fix error reporting number of nonlinear discrete variables --- pyomo/repn/plugins/nl_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ee5b65149ae..d629da2ee87 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1277,8 +1277,8 @@ def write(self, model): len(linear_binary_vars), len(linear_integer_vars), len(both_vars_nonlinear.intersection(discrete_vars)), - len(con_vars_nonlinear.intersection(discrete_vars)), - len(obj_vars_nonlinear.intersection(discrete_vars)), + len(con_only_nonlinear_vars.intersection(discrete_vars)), + len(obj_only_nonlinear_vars.intersection(discrete_vars)), ) ) # From be3ca3192d5b9a91a9834f001fa07b352b30b8aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:09:50 -0600 Subject: [PATCH 0914/1178] nlv2: map integer variables over [0,1] t binary --- pyomo/repn/plugins/nl_writer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index d629da2ee87..ab74f0ab44d 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -113,6 +113,7 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf +zero_one = {0, 1} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL @@ -882,7 +883,13 @@ def write(self, model): elif v.is_binary(): binary_vars.add(_id) elif v.is_integer(): - integer_vars.add(_id) + bnd = var_bounds[_id] + # Note: integer variables whose bounds are in {0, 1} + # should be classified as binary + if bnd[1] in zero_one and bnd[0] in zero_one: + binary_vars.add(_id) + else: + integer_vars.add(_id) else: raise ValueError( f"Variable '{v.name}' has a domain that is not Real, " From 2f585db8187952149c0974d14e6ce99ab20b4f31 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:10:09 -0600 Subject: [PATCH 0915/1178] nlv2: add tests for variable categorization --- pyomo/repn/tests/ampl/test_nlv2.py | 134 ++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index be72025edcd..0f2bacaea8b 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -42,6 +42,8 @@ Suffix, Constraint, Expression, + Binary, + Integers, ) import pyomo.environ as pyo @@ -1266,7 +1268,7 @@ def test_nonfloat_constants(self): 0 0 #network constraints: nonlinear, linear 0 0 0 #nonlinear vars in constraints, objectives, both 0 0 0 1 #linear network variables; functions; arith, flags - 0 4 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 4 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) 4 4 #nonzeros in Jacobian, obj. gradient 6 4 #max name lengths: constraints, variables 0 0 0 0 0 #common exprs: b,c,o,c1,o1 @@ -2165,6 +2167,136 @@ def test_named_expressions(self): 0 0 1 0 2 0 +""", + OUT.getvalue(), + ) + ) + + def test_discrete_var_tabulation(self): + # This tests an error reported in #3235 + # + # Among other issues, this verifies that nonlinear discrete + # variables are tabulated correctly (header line 7), and that + # integer variables with bounds in {0, 1} are mapped to binary + # variables. + m = ConcreteModel() + m.p1 = Var(bounds=(0.85, 1.15)) + m.p2 = Var(bounds=(0.68, 0.92)) + m.c1 = Var(bounds=(-0.0, 0.7)) + m.c2 = Var(bounds=(-0.0, 0.7)) + m.t1 = Var(within=Binary, bounds=(0, 1)) + m.t2 = Var(within=Binary, bounds=(0, 1)) + m.t3 = Var(within=Binary, bounds=(0, 1)) + m.t4 = Var(within=Binary, bounds=(0, 1)) + m.t5 = Var(within=Integers, bounds=(0, None)) + m.t6 = Var(within=Integers, bounds=(0, None)) + m.x1 = Var(within=Binary) + m.x2 = Var(within=Integers, bounds=(0, 1)) + m.x3 = Var(within=Integers, bounds=(0, None)) + m.const = Constraint(expr=((0.7 - (m.c1*m.t1 + m.c2*m.t2)) <= (m.p1*m.t1 + m.p2*m.t2 + m.p1*m.t4 + m.t6*m.t5))) + m.OBJ = Objective(expr=(m.p1*m.t1 + m.p2*m.t2 + m.p2*m.t3 + m.x1 + m.x2 + m.x3)) + + OUT = io.StringIO() + nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 13 1 1 0 0 #vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 9 10 4 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 2 1 2 3 1 #discrete variables: binary, integer, nonlinear (b,c,o) + 9 8 #nonzeros in Jacobian, obj. gradient + 5 2 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #const +o0 #+ +o16 #- +o0 #+ +o2 #* +v4 #c1 +v2 #t1 +o2 #* +v5 #c2 +v3 #t2 +o16 #- +o54 #sumlist +4 #(n) +o2 #* +v0 #p1 +v2 #t1 +o2 #* +v1 #p2 +v3 #t2 +o2 #* +v0 #p1 +v6 #t4 +o2 #* +v7 #t6 +v8 #t5 +O0 0 #OBJ +o54 #sumlist +3 #(n) +o2 #* +v0 #p1 +v2 #t1 +o2 #* +v1 #p2 +v3 #t2 +o2 #* +v1 #p2 +v9 #t3 +x0 #initial guess +r #1 ranges (rhs's) +1 -0.7 #const +b #13 bounds (on variables) +0 0.85 1.15 #p1 +0 0.68 0.92 #p2 +0 0 1 #t1 +0 0 1 #t2 +0 -0.0 0.7 #c1 +0 -0.0 0.7 #c2 +0 0 1 #t4 +2 0 #t6 +2 0 #t5 +0 0 1 #t3 +0 0 1 #x1 +0 0 1 #x2 +2 0 #x3 +k12 #intermediate Jacobian column lengths +1 +2 +3 +4 +5 +6 +7 +8 +9 +9 +9 +9 +J0 9 #const +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +G0 8 #OBJ +0 0 +1 0 +2 0 +3 0 +9 0 +10 1 +11 1 +12 1 """, OUT.getvalue(), ) From 25a7344df15fc60378a168a1adfb18496873f375 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:25:10 -0600 Subject: [PATCH 0916/1178] NFC: apply black --- pyomo/repn/tests/ampl/test_nlv2.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 0f2bacaea8b..27d129ca886 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2193,8 +2193,15 @@ def test_discrete_var_tabulation(self): m.x1 = Var(within=Binary) m.x2 = Var(within=Integers, bounds=(0, 1)) m.x3 = Var(within=Integers, bounds=(0, None)) - m.const = Constraint(expr=((0.7 - (m.c1*m.t1 + m.c2*m.t2)) <= (m.p1*m.t1 + m.p2*m.t2 + m.p1*m.t4 + m.t6*m.t5))) - m.OBJ = Objective(expr=(m.p1*m.t1 + m.p2*m.t2 + m.p2*m.t3 + m.x1 + m.x2 + m.x3)) + m.const = Constraint( + expr=( + (0.7 - (m.c1 * m.t1 + m.c2 * m.t2)) + <= (m.p1 * m.t1 + m.p2 * m.t2 + m.p1 * m.t4 + m.t6 * m.t5) + ) + ) + m.OBJ = Objective( + expr=(m.p1 * m.t1 + m.p2 * m.t2 + m.p2 * m.t3 + m.x1 + m.x2 + m.x3) + ) OUT = io.StringIO() nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) From 6b1e2960e94172a10802326b6e7b848c4c7b4ab4 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 17 Apr 2024 00:36:04 -0400 Subject: [PATCH 0917/1178] fix test pr highs version bug --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9eb6d362bb8..711e134e401 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -606,7 +606,7 @@ jobs: shell: bash run: | echo "NOTE: temporarily pinning to highspy pre-release for testing" - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy>=1.7.1.dev1 \ + $PYTHON_EXE -m pip install --cache-dir cache/pip "highspy>=1.7.1.dev1" \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking From 76ba0eb525b5a2037220b84e17edde93715cb599 Mon Sep 17 00:00:00 2001 From: MAiNGO-github <139969768+MAiNGO-github@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:29:14 +0200 Subject: [PATCH 0918/1178] Update pyomo/contrib/appsi/solvers/maingo.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 29464e6a876..017841d1b8c 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -384,7 +384,7 @@ def _postsolve(self, timer: HierarchicalTimer): results.termination_condition = TerminationCondition.optimal if status == maingopy.FEASIBLE_POINT: logger.warning( - "MAiNGO did only find a feasible solution but did not prove its global optimality." + "MAiNGO found a feasible solution but did not prove its global optimality." ) elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible From f9ada2946d295da4d53e56c3a7cc2f78e5bbaa1e Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:47:11 +0200 Subject: [PATCH 0919/1178] Restrict y to positive values (MAiNGO cannot handle 1/0) --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index c063adc2bfe..58806d1e86c 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -688,7 +688,7 @@ def test_fixed_vars_4( raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() - m.y = pe.Var() + m.y = pe.Var(bounds=(1e-6, None)) m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) From 42fcd17429cc2b608cc26544dc19c8cd3074bbb9 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:48:15 +0200 Subject: [PATCH 0920/1178] Restrict y to positive values (MAiNGO cannot handle log(negative)) --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 58806d1e86c..5a46c1d3e5b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -858,7 +858,7 @@ def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() - m.x = pe.Var(initialize=1) + m.x = pe.Var(initialize=1, bounds=(1e-6, None)) m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) From bec9be7e9e3f4c5d221ddb076ddcf7697616182b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:49:48 +0200 Subject: [PATCH 0921/1178] Exluded MAiNGO for checking unbounded termination criterion --- .../solvers/tests/test_persistent_solvers.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 5a46c1d3e5b..660ba60f26f 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1090,13 +1090,14 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - self.assertIn( - res.termination_condition, - { - TerminationCondition.unbounded, - TerminationCondition.infeasibleOrUnbounded, - }, - ) + if not isinstance(opt, MAiNGO): + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) m.obj.sense = pe.minimize opt.config.load_solution = True m.obj = pe.Objective(expr=m.x * m.y) From 6db9970a41760b64cf5e43ddc73eb3f8eb0aefc0 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:54:44 +0200 Subject: [PATCH 0922/1178] Create MAiNGOTest class with tighter tolerances to pass tests --- pyomo/contrib/appsi/solvers/maingo.py | 16 ++++++++++++++++ .../solvers/tests/test_persistent_solvers.py | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 29464e6a876..bff8d6f594e 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -201,6 +201,9 @@ def _solve(self, timer: HierarchicalTimer): self._mymaingo.set_option("loggingDestination", 2) self._mymaingo.set_log_file_name(config.logfile) + self._mymaingo.set_option("epsilonA", 1e-4) + self._mymaingo.set_option("epsilonR", 1e-4) + self._set_maingo_options() if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) @@ -480,3 +483,16 @@ def get_reduced_costs(self, vars_to_load=None): def get_duals(self, cons_to_load=None): raise ValueError("MAiNGO does not support returning Duals") + + + def _set_maingo_options(self): + pass + + +# Solver class with tighter tolerances for testing +class MAiNGOTest(MAiNGO): + def _set_maingo_options(self): + self._mymaingo.set_option("epsilonA", 1e-8) + self._mymaingo.set_option("epsilonR", 1e-8) + self._mymaingo.set_option("deltaIneq", 1e-9) + self._mymaingo.set_option("deltaEq", 1e-9) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 660ba60f26f..23440065491 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,7 +17,8 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import MAiNGOTest as MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os From de7fc5c68360bb1c49d351eca21ffff3d7cf4d60 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:56:01 +0200 Subject: [PATCH 0923/1178] Exclude duals and RCs for tests with MAiNGO --- .../solvers/tests/test_persistent_solvers.py | 176 ++++++++++-------- 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 23440065491..f50461af373 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -185,14 +185,16 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( @@ -209,9 +211,10 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 3) - self.assertAlmostEqual(rc[m.y], 4) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( @@ -226,14 +229,16 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( @@ -265,9 +270,10 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( @@ -303,9 +309,10 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( @@ -337,9 +344,10 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( @@ -407,9 +415,10 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0) - self.assertAlmostEqual(duals[m.c2], 0) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( @@ -436,9 +445,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) @@ -447,10 +457,11 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) - self.assertAlmostEqual(duals[m.c2], 0) - self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) del m.c3 res = opt.solve(m) @@ -459,9 +470,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( @@ -500,14 +512,15 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() + if not opt_class is MAiNGO: + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -524,13 +537,14 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertAlmostEqual(duals[m.c2], 0.5) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) - duals = opt.get_duals(cons_to_load=[m.c1]) - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertNotIn(m.c2, duals) + duals = opt.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( @@ -778,17 +792,19 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( @@ -986,24 +1002,25 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - reduced_costs = res.solution_loader.get_reduced_costs() - self.assertIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.x], 1) - self.assertAlmostEqual(reduced_costs[m.y], 0) - reduced_costs = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.y], 0) - duals = res.solution_loader.get_duals() - self.assertIn(m.c1, duals) - self.assertIn(m.c2, duals) - self.assertAlmostEqual(duals[m.c1], 1) - self.assertAlmostEqual(duals[m.c2], 0) - duals = res.solution_loader.get_duals([m.c1]) - self.assertNotIn(m.c2, duals) - self.assertIn(m.c1, duals) - self.assertAlmostEqual(duals[m.c1], 1) + if not opt_class is MAiNGO: + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( @@ -1373,7 +1390,8 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if not opt_class is MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: @@ -1385,8 +1403,9 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): @@ -1397,11 +1416,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if not opt_class is MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - self.assertNotIn(m.c, m.dual) + if not opt_class is MAiNGO: + self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - self.assertAlmostEqual(m.dual[m.c], 1) + if not opt_class is MAiNGO: + self.assertAlmostEqual(m.dual[m.c], 1) From 334b06762bbae40d954ae0ca54720f4f6c448893 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:56:31 +0200 Subject: [PATCH 0924/1178] MAiNGOTest class in __init__ --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index 352571b98f8..c1ebdf28780 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO +from .maingo import MAiNGO, MAiNGOTest From a1e6b92c7518027f8234069acedc9dac86d8b04a Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:57:07 +0200 Subject: [PATCH 0925/1178] Add copyright statement --- pyomo/contrib/appsi/solvers/maingo.py | 11 +++++++++++ pyomo/contrib/appsi/solvers/maingo_solvermodel.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index bff8d6f594e..d48e9874712 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections import namedtuple import logging import math diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index 4abc53ae290..686d7c54657 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math from pyomo.common.dependencies import attempt_import From 1fab90b84754bd652263a8eed467f081d0de603a Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:58:03 +0200 Subject: [PATCH 0926/1178] Set default for ConfigValues --- pyomo/contrib/appsi/solvers/maingo.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d48e9874712..ee85d4549a5 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -98,13 +98,11 @@ def __init__( visibility=visibility, ) - self.declare("logfile", ConfigValue(domain=str)) - self.declare("solver_output_logger", ConfigValue()) - self.declare("log_level", ConfigValue(domain=NonNegativeInt)) - - self.logfile = "" - self.solver_output_logger = logger - self.log_level = logging.INFO + self.declare("logfile", ConfigValue(domain=str, default="")) + self.declare("solver_output_logger", ConfigValue(default=logger)) + self.declare( + "log_level", ConfigValue(domain=NonNegativeInt, default=logging.INFO) + ) class MAiNGOSolutionLoader(PersistentSolutionLoader): From 5b70135f03575af6c1ef6ddce6bd98c12846194e Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:58:30 +0200 Subject: [PATCH 0927/1178] Remove check for Python version --- pyomo/contrib/appsi/solvers/maingo.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index ee85d4549a5..0886ce21c75 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -151,15 +151,9 @@ def available(self): return self._available def version(self): - # Check if Python >= 3.8 - if sys.version_info.major >= 3 and sys.version_info.minor >= 8: - from importlib.metadata import version + import pkg_resources - version = version('maingopy') - else: - import pkg_resources - - version = pkg_resources.get_distribution('maingopy').version + version = pkg_resources.get_distribution('maingopy').version return tuple(int(k) for k in version.split('.')) From 55b7dff375cc9c82cd2d425698c521090711a816 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 16:00:41 +0200 Subject: [PATCH 0928/1178] Add missing functionalities and fix bugs --- pyomo/contrib/appsi/solvers/maingo.py | 81 ++++++++++++++----- .../appsi/solvers/maingo_solvermodel.py | 51 ++++++++---- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 0886ce21c75..eabfdd36267 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -120,6 +120,7 @@ def __init__(self, solver): super(MAiNGOResults, self).__init__() self.wallclock_time = None self.cpu_time = None + self.globally_optimal = None self.solution_loader = MAiNGOSolutionLoader(solver=solver) @@ -228,9 +229,14 @@ def solve(self, model, timer: HierarchicalTimer = None): self._last_results_object.solution_loader.invalidate() if timer is None: timer = HierarchicalTimer() - timer.start("set_instance") - self.set_instance(model) - timer.stop("set_instance") + if model is not self._model: + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + else: + timer.start("Update") + self.update(timer=timer) + timer.stop("Update") res = self._solve(timer) self._last_results_object = res if self.config.report_timing: @@ -285,7 +291,7 @@ def _process_domain_and_bounds(self, var): return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - for ndx, var in enumerate(variables): + for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds(var) self._maingo_vars.append( @@ -331,10 +337,11 @@ def set_instance(self, model): con_list=self._cons, objective=self._objective, idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, ) def _add_constraints(self, cons: List[_GeneralConstraintData]): - self._cons = cons + self._cons += cons def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: @@ -344,7 +351,8 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass + for con in cons: + self._cons.remove(con) def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: @@ -354,28 +362,48 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): pass def _remove_variables(self, variables: List[_GeneralVarData]): - pass + removed_maingo_vars = [] + for var in variables: + varname = self._symbol_map.getSymbol(var, self._labeler) + del self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] + removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] + del self._pyomo_var_to_solver_var_id_map[id(var)] + + for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): + # How many variables before current var where removed? + num_removed = 0 + for removed_var in removed_maingo_vars: + if removed_var <= maingo_var_id: + num_removed += 1 + self._pyomo_var_to_solver_var_id_map[pyomo_var] = ( + maingo_var_id - num_removed + ) def _remove_params(self, params: List[_ParamData]): pass def _update_variables(self, variables: List[_GeneralVarData]): - pass + for var in variables: + if id(var) not in self._pyomo_var_to_solver_var_id_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] = ( + MaingoVar(name=var.name, type=vtype, lb=lb, ub=ub, init=var.value) + ) def update_params(self): - pass + vars = [var[0] for var in self._vars.values()] + self._update_variables(vars) def _set_objective(self, obj): - if obj is None: - raise NotImplementedError( - "MAiNGO needs a objective. Please set a dummy objective." - ) - else: - if not obj.sense in {minimize, maximize}: - raise ValueError( - "Objective sense is not recognized: {0}".format(obj.sense) - ) - self._objective = obj + + if not obj.sense in {minimize, maximize}: + raise ValueError("Objective sense is not recognized: {0}".format(obj.sense)) + self._objective = obj def _postsolve(self, timer: HierarchicalTimer): config = self.config @@ -388,7 +416,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal + results.globally_optimal = True if status == maingopy.FEASIBLE_POINT: + results.globally_optimal = False logger.warning( "MAiNGO did only find a feasible solution but did not prove its global optimality." ) @@ -425,8 +455,8 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start("load solution") if config.load_solution: - if not results.best_feasible_objective is None: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition is TerminationCondition.optimal: + if not results.globally_optimal: logger.warning( "Loading a feasible but suboptimal solution. " "Please set load_solution=False and check " @@ -487,6 +517,15 @@ def get_reduced_costs(self, vars_to_load=None): def get_duals(self, cons_to_load=None): raise ValueError("MAiNGO does not support returning Duals") + def update(self, timer: HierarchicalTimer = None): + super(MAiNGO, self).update(timer=timer) + self._solver_model = maingo_solvermodel.SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, + ) def _set_maingo_options(self): pass diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index 686d7c54657..ca746c4a9b7 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -13,6 +13,7 @@ from pyomo.common.dependencies import attempt_import from pyomo.core.base.var import ScalarVar +from pyomo.core.base.expression import ScalarExpression import pyomo.core.expr.expr_common as common import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import ( @@ -178,19 +179,14 @@ def visiting_potential_leaf(self, node): return True, maingo_var def _monomial_to_maingo(self, node): - if node.__class__ is ScalarVar: - var = node - const = 1 - else: - const, var = node.args - maingo_var_id = self.idmap[id(var)] - maingo_var = self.variables[maingo_var_id] + const, var = node.args if const.__class__ not in native_types: const = value(const) if var.is_fixed(): return const * var.value if not const: return 0 + maingo_var = self._var_to_maingo(var) if const in _plusMinusOne: if const < 0: return -maingo_var @@ -198,12 +194,25 @@ def _monomial_to_maingo(self, node): return maingo_var return const * maingo_var + def _var_to_maingo(self, var): + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + return maingo_var + def _linear_to_maingo(self, node): values = [ ( self._monomial_to_maingo(arg) - if (arg.__class__ in {EXPR.MonomialTermExpression, ScalarVar}) - else (value(arg)) + if (arg.__class__ is EXPR.MonomialTermExpression) + else ( + value(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_maingo(arg) + if arg.is_variable_type() + else value(arg) + ) + ) ) for arg in node.args ] @@ -211,17 +220,25 @@ def _linear_to_maingo(self, node): class SolverModel(maingopy.MAiNGOmodel): - def __init__(self, var_list, objective, con_list, idmap): + def __init__(self, var_list, objective, con_list, idmap, logger): maingopy.MAiNGOmodel.__init__(self) self._var_list = var_list self._con_list = con_list self._objective = objective self._idmap = idmap + self._logger = logger + self._no_objective = False + + if self._objective is None: + self._logger.warning("No objective given, setting a dummy objective of 1.") + self._no_objective = True def build_maingo_objective(self, obj, visitor): + if self._no_objective: + return visitor.variables[-1] maingo_obj = visitor.dfs_postorder_stack(obj.expr) if obj.sense == maximize: - maingo_obj *= -1 + return -1 * maingo_obj return maingo_obj def build_maingo_constraints(self, cons, visitor): @@ -235,7 +252,7 @@ def build_maingo_constraints(self, cons, visitor): ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] elif con.has_ub(): ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - elif con.has_ub(): + elif con.has_lb(): ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] else: raise ValueError( @@ -245,18 +262,24 @@ def build_maingo_constraints(self, cons, visitor): return eqs, ineqs def get_variables(self): - return [ + vars = [ maingopy.OptimizationVariable( maingopy.Bounds(var.lb, var.ub), var.type, var.name ) for var in self._var_list ] + if self._no_objective: + vars += [maingopy.OptimizationVariable(maingopy.Bounds(1, 1), "dummy_obj")] + return vars def get_initial_point(self): - return [ + initial = [ var.init if not var.init is None else (var.lb + var.ub) / 2.0 for var in self._var_list ] + if self._no_objective: + initial += [1] + return initial def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) From 380f32cb5b1a044002c7c93f5fac394c5ca9c3e2 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:28:54 +0200 Subject: [PATCH 0929/1178] Register MAiNGO in SolverFactory --- pyomo/contrib/appsi/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index fbe81484eba..3e1b639ce3b 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -11,7 +11,7 @@ from pyomo.common.extensions import ExtensionBuilderFactory from .base import SolverFactory -from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs +from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs, MAiNGO from .build import AppsiBuilder @@ -30,3 +30,6 @@ def load(): SolverFactory.register(name='highs', doc='Automated persistent interface to Highs')( Highs ) + SolverFactory.register( + name='maingo', doc='Automated persistent interface to MAiNGO' + )(MAiNGO) From cf560a626c56aac304c269c02c06d783c42957c0 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:31:50 +0200 Subject: [PATCH 0930/1178] Reformulate confusing comment --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d542542f543..f95a943bed3 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -369,8 +369,8 @@ def _remove_variables(self, variables: List[_GeneralVarData]): removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] del self._pyomo_var_to_solver_var_id_map[id(var)] + # Update _pyomo_var_to_solver_var_id_map to account for removed variables for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): - # How many variables before current var where removed? num_removed = 0 for removed_var in removed_maingo_vars: if removed_var <= maingo_var_id: From 3e477c929883d8a555fdde8afeb99b435f0be829 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:33:45 +0200 Subject: [PATCH 0931/1178] Skip tests with problematic log and 1/x --- .../appsi/solvers/tests/test_persistent_solvers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index f50461af373..b569b305a07 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -699,11 +699,11 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available(): + if not opt.available() or opt_class in MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() - m.y = pe.Var(bounds=(1e-6, None)) + m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) @@ -872,10 +872,10 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class in MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() - m.x = pe.Var(initialize=1, bounds=(1e-6, None)) + m.x = pe.Var(initialize=1) m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) From d892473bca89ae62fd69b53eb0585b769cd91a60 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:42:04 +0200 Subject: [PATCH 0932/1178] Add maingo to options --- pyomo/contrib/appsi/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..13a841437ac 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1665,7 +1665,7 @@ def license_is_valid(self) -> bool: @property def options(self): - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): return getattr(self, solver_name + '_options') raise NotImplementedError('Could not find the correct options') @@ -1673,7 +1673,7 @@ def options(self): @options.setter def options(self, val): found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): setattr(self, solver_name + '_options', val) found = True From d026ee135f72788536b0586e6815fcc561a0ba86 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:42:30 +0200 Subject: [PATCH 0933/1178] Rewrite check for MAiNGO --- .../solvers/tests/test_persistent_solvers.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index b569b305a07..2207ba70f4e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -185,14 +185,14 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -211,7 +211,7 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 3) self.assertAlmostEqual(rc[m.y], 4) @@ -229,14 +229,14 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -270,7 +270,7 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -309,7 +309,7 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -344,7 +344,7 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @@ -415,7 +415,7 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @@ -445,7 +445,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -457,7 +457,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -470,7 +470,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -512,7 +512,7 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - if not opt_class is MAiNGO: + if opt_class != MAiNGO: with self.assertRaisesRegex( RuntimeError, '.*does not currently have valid duals.*' ): @@ -537,7 +537,7 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0.5) self.assertAlmostEqual(duals[m.c2], 0.5) @@ -699,7 +699,7 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available() or opt_class in MAiNGO: + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() @@ -792,7 +792,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -801,7 +801,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -872,7 +872,7 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available() or opt_class in MAiNGO: + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(initialize=1) @@ -1002,7 +1002,7 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: reduced_costs = res.solution_loader.get_reduced_costs() self.assertIn(m.x, reduced_costs) self.assertIn(m.y, reduced_costs) @@ -1108,7 +1108,7 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - if not isinstance(opt, MAiNGO): + if opt_class != MAiNGO: self.assertIn( res.termination_condition, { @@ -1390,7 +1390,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] @@ -1403,7 +1403,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @@ -1416,14 +1416,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertAlmostEqual(m.dual[m.c], 1) From cf3c9da2a17cff71953e2ced4f24f999835542ba Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 12:26:32 +0200 Subject: [PATCH 0934/1178] Add ConfigDict for tolerances, change test tolerances --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/maingo.py | 60 ++++++++++++++----- .../solvers/tests/test_persistent_solvers.py | 27 ++++----- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c1ebdf28780..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO, MAiNGOTest +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index f95a943bed3..944673be53d 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -25,7 +25,12 @@ ) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.collections import ComponentMap -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ( + ConfigValue, + ConfigDict, + NonNegativeInt, + NonNegativeFloat, +) from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException from pyomo.common.log import LogStream @@ -97,7 +102,41 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) + self.tolerances: ConfigDict = self.declare( + 'tolerances', ConfigDict(implicit=True) + ) + + self.tolerances.epsilonA: Optional[float] = self.tolerances.declare( + 'epsilonA', + ConfigValue( + domain=NonNegativeFloat, + default=1e-4, + description="Absolute optimality tolerance", + ), + ) + self.tolerances.epsilonR: Optional[float] = self.tolerances.declare( + 'epsilonR', + ConfigValue( + domain=NonNegativeFloat, + default=1e-4, + description="Relative optimality tolerance", + ), + ) + self.tolerances.deltaEq: Optional[float] = self.tolerances.declare( + 'deltaEq', + ConfigValue( + domain=NonNegativeFloat, default=1e-6, description="Equality tolerance" + ), + ) + self.tolerances.deltaIneq: Optional[float] = self.tolerances.declare( + 'deltaIneq', + ConfigValue( + domain=NonNegativeFloat, + default=1e-6, + description="Inequality tolerance", + ), + ) self.declare("logfile", ConfigValue(domain=str, default="")) self.declare("solver_output_logger", ConfigValue(default=logger)) self.declare( @@ -205,9 +244,10 @@ def _solve(self, timer: HierarchicalTimer): self._mymaingo.set_option("loggingDestination", 2) self._mymaingo.set_log_file_name(config.logfile) - self._mymaingo.set_option("epsilonA", 1e-4) - self._mymaingo.set_option("epsilonR", 1e-4) - self._set_maingo_options() + self._mymaingo.set_option("epsilonA", config.tolerances.epsilonA) + self._mymaingo.set_option("epsilonR", config.tolerances.epsilonR) + self._mymaingo.set_option("deltaEq", config.tolerances.deltaEq) + self._mymaingo.set_option("deltaIneq", config.tolerances.deltaIneq) if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) @@ -526,15 +566,3 @@ def update(self, timer: HierarchicalTimer = None): idmap=self._pyomo_var_to_solver_var_id_map, logger=logger, ) - - def _set_maingo_options(self): - pass - - -# Solver class with tighter tolerances for testing -class MAiNGOTest(MAiNGO): - def _set_maingo_options(self): - self._mymaingo.set_option("epsilonA", 1e-8) - self._mymaingo.set_option("epsilonR", 1e-8) - self._mymaingo.set_option("deltaIneq", 1e-9) - self._mymaingo.set_option("deltaEq", 1e-9) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 2207ba70f4e..d6df1710a03 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,8 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs -from pyomo.contrib.appsi.solvers import MAiNGOTest as MAiNGO +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -866,8 +865,8 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) res = opt.solve(m) - self.assertAlmostEqual(m.x.value, -0.42630274815985264) - self.assertAlmostEqual(m.y.value, 0.6529186341994245) + self.assertAlmostEqual(m.x.value, -0.42630274815985264, 6) + self.assertAlmostEqual(m.y.value, 0.6529186341994245, 6) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -1212,19 +1211,19 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( @@ -1248,16 +1247,16 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.use_extensions = True res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) @parameterized.expand(input=all_solvers) def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): From e0b6277f72fb00347d8ed482a534e5471ffd9688 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Apr 2024 07:20:31 -0600 Subject: [PATCH 0935/1178] Attempt installing mpich first --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index a15240194f2..ce7d03f6898 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -92,7 +92,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py==3.1.5 + PACKAGES: mpich mpi4py - os: ubuntu-latest python: '3.10' From d4aced699a5ac0466ebcb375a05249bfabf25901 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Apr 2024 07:51:59 -0600 Subject: [PATCH 0936/1178] Switch to openmpi --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index ce7d03f6898..1885f6a00e2 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -92,7 +92,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpich mpi4py + PACKAGES: openmpi mpi4py - os: ubuntu-latest python: '3.10' From 13830955da64e801bd61a583582783e48243c742 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 08:47:04 -0600 Subject: [PATCH 0937/1178] Remove unused import --- pyomo/opt/results/problem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 34da8f91918..a8eca1e3b41 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -12,7 +12,6 @@ import enum from pyomo.opt.results.container import MapContainer -from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.common.enums import ExtendedEnumType, ObjectiveSense From 0e68f4bab1adc62e29652f2005d787fb31fc1ec7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 08:58:17 -0600 Subject: [PATCH 0938/1178] nlv2: simplify / improve performance of binary domain check --- pyomo/repn/plugins/nl_writer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ab74f0ab44d..207846787fd 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -113,7 +113,7 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf -zero_one = {0, 1} +allowable_binary_var_bounds = {(0,0), (0,1), (1,1)} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL @@ -883,10 +883,9 @@ def write(self, model): elif v.is_binary(): binary_vars.add(_id) elif v.is_integer(): - bnd = var_bounds[_id] # Note: integer variables whose bounds are in {0, 1} # should be classified as binary - if bnd[1] in zero_one and bnd[0] in zero_one: + if var_bounds[_id] in allowable_binary_var_bounds: binary_vars.add(_id) else: integer_vars.add(_id) From ee05e18625ba7193e6cf739bccd66cb773fc7d2d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:05:46 -0600 Subject: [PATCH 0939/1178] NFC: apply black --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 207846787fd..86da2a3622b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -113,7 +113,7 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf -allowable_binary_var_bounds = {(0,0), (0,1), (1,1)} +allowable_binary_var_bounds = {(0, 0), (0, 1), (1, 1)} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL From 232be203e8669d174b6aaa97ecfa0ddd1d639eeb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:52:27 -0600 Subject: [PATCH 0940/1178] Remove duplicate imports --- pyomo/core/base/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 3d1347659db..341af677b0e 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -84,7 +84,6 @@ from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, - BooleanVarData, BooleanVarList, ScalarBooleanVar, ) @@ -145,7 +144,7 @@ active_import_suffix_generator, Suffix, ) -from pyomo.core.base.var import Var, VarData, VarData, ScalarVar, VarList +from pyomo.core.base.var import Var, VarData, ScalarVar, VarList from pyomo.core.base.instance2dat import instance2dat From 354cadf178d6210ebe386687ff1af15d8481c253 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:53:00 -0600 Subject: [PATCH 0941/1178] Revert accidental test name change --- pyomo/core/tests/unit/test_suffix.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 70f028a3eff..d2e861cceb5 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1567,7 +1567,7 @@ def test_clone_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_cloneObjectiveData(self): + def test_clone_ObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=lambda model, i: model.x[i]) @@ -1603,7 +1603,7 @@ def test_clone_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_cloneBlockData(self): + def test_clone_BlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() @@ -1725,7 +1725,7 @@ def test_pickle_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_pickleObjectiveData(self): + def test_pickle_ObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=simple_obj_rule) @@ -1761,7 +1761,7 @@ def test_pickle_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_pickleBlockData(self): + def test_pickle_BlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() From 37915b4b409c3e468d70e0ed64faaee0b7c83ef5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:53:59 -0600 Subject: [PATCH 0942/1178] Fix docstring, add Objectives as known type to FBBT --- pyomo/contrib/fbbt/fbbt.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index eb7155313c4..1507c4a3cc5 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -24,9 +24,10 @@ import math from pyomo.core.base.block import Block from pyomo.core.base.constraint import Constraint +from pyomo.core.base.expression import ExpressionData, ScalarExpression +from pyomo.core.base.objective import ObjectiveData, ScalarObjective from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import ExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( @@ -340,7 +341,7 @@ def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): Parameters ---------- visitor: _FBBTVisitorLeafToRoot - node: pyomo.core.base.expression.ExpressionData + node: pyomo.core.base.expression.NamedExpressionData expr: NamedExpressionData arg """ bnds_dict = visitor.bnds_dict @@ -368,6 +369,8 @@ def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, ExpressionData: _prop_bnds_leaf_to_root_NamedExpression, ScalarExpression: _prop_bnds_leaf_to_root_NamedExpression, + ObjectiveData: _prop_bnds_leaf_to_root_NamedExpression, + ScalarObjective: _prop_bnds_leaf_to_root_NamedExpression, }, ) @@ -904,7 +907,7 @@ def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): Parameters ---------- - node: pyomo.core.base.expression.ExpressionData + node: pyomo.core.base.expression.NamedExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -947,6 +950,8 @@ def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): _prop_bnds_root_to_leaf_map[ExpressionData] = _prop_bnds_root_to_leaf_NamedExpression _prop_bnds_root_to_leaf_map[ScalarExpression] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ObjectiveData] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ScalarObjective] = _prop_bnds_root_to_leaf_NamedExpression def _check_and_reset_bounds(var, lb, ub): From e35d537d30e6e28d2c54737408adcf24d7ae361d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:54:18 -0600 Subject: [PATCH 0943/1178] Fix docstring --- pyomo/core/base/constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 3a71758d55d..eb4af76fdc1 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -127,7 +127,7 @@ def C_rule(model, i, j): class ConstraintData(ActiveComponentData): """ - This class defines the data for a single general constraint. + This class defines the data for a single algebraic constraint. Constructor arguments: component The Constraint object that owns this data. From 256d5bb8db73c11fb75679ed8b5770f99b6820d3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Apr 2024 11:30:24 -0600 Subject: [PATCH 0944/1178] Update PR test --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2615f6b838e..619a5e695e2 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -93,7 +93,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py==3.1.5 + PACKAGES: openmpi mpi4py - os: ubuntu-latest python: '3.11' From 31474401ef9034b90946ca4a094f45b89b3f912a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20J=C3=BAnior?= <16216517+henriquejsfj@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:19:02 -0300 Subject: [PATCH 0945/1178] Fix: Get SCIP solving time considering float number with some text This fix does not change previous behavior and handles the case of a string with a float number plus some text. --- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index fd69954b428..966fb1e1a1d 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -455,7 +455,7 @@ def read_scip_log(filename: str): solver_status = scip_lines[0][colon_position + 2 : scip_lines[0].index('\n')] solving_time = float( - scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')] + scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')].split(' ')[0] ) try: From 3ce74167beac24f4ffd963fef8d0f57a0979ea69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20J=C3=BAnior?= <16216517+henriquejsfj@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:05:40 -0300 Subject: [PATCH 0946/1178] Test the Scip reoptimization option --- pyomo/solvers/tests/mip/test_scip.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/solvers/tests/mip/test_scip.py b/pyomo/solvers/tests/mip/test_scip.py index 01de0d16826..ad54daeddc0 100644 --- a/pyomo/solvers/tests/mip/test_scip.py +++ b/pyomo/solvers/tests/mip/test_scip.py @@ -106,6 +106,12 @@ def test_scip_solve_from_instance_options(self): results.write(filename=_out, times=False, format='json') self.compare_json(_out, join(currdir, "test_scip_solve_from_instance.baseline")) + def test_scip_solve_from_instance_with_reoptimization(self): + # Test scip with re-optimization option enabled + # This case changes the Scip output results which may break the results parser + self.scip.options['reoptimization/enable'] = True + self.test_scip_solve_from_instance() + if __name__ == "__main__": deleteFiles = False From 7537bb517b2bc9b841ad368a433b63f0700e9d86 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 13:54:42 -0600 Subject: [PATCH 0947/1178] Propogating domain to substitution var in the var aggregator --- pyomo/contrib/preprocessing/plugins/var_aggregator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/preprocessing/plugins/var_aggregator.py b/pyomo/contrib/preprocessing/plugins/var_aggregator.py index d862f167fd7..5b714cf439d 100644 --- a/pyomo/contrib/preprocessing/plugins/var_aggregator.py +++ b/pyomo/contrib/preprocessing/plugins/var_aggregator.py @@ -13,7 +13,8 @@ from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.core.base import Block, Constraint, VarList, Objective, TransformationFactory +from pyomo.core.base import (Block, Constraint, VarList, Objective, Reals, + TransformationFactory) from pyomo.core.expr import ExpressionReplacementVisitor from pyomo.core.expr.numvalue import value from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation @@ -248,6 +249,12 @@ def _apply_to(self, model, detect_fixed_vars=True): # the variables in its equality set. z_agg.setlb(max_if_not_None(v.lb for v in eq_set if v.has_lb())) z_agg.setub(min_if_not_None(v.ub for v in eq_set if v.has_ub())) + # Set the domain of the aggregate variable to the intersection of + # the domains of the variables in its equality set + domain = Reals + for v in eq_set: + domain = domain & v.domain + z_agg.domain = domain # Set the fixed status of the aggregate var fixed_vars = [v for v in eq_set if v.fixed] From fe6ab256e9cc53c190161dea836b8b998716bb66 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 13:54:59 -0600 Subject: [PATCH 0948/1178] Testing var aggregator with vars with different domains --- .../tests/test_var_aggregator.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index 6f6d02f2180..ff1ea843d23 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -19,12 +19,16 @@ max_if_not_None, min_if_not_None, ) +from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.environ import ( + Binary, ConcreteModel, Constraint, ConstraintList, + maximize, Objective, RangeSet, + Reals, SolverFactory, TransformationFactory, Var, @@ -210,6 +214,43 @@ def test_var_update(self): self.assertEqual(m.x.value, 0) self.assertEqual(m.y.value, 0) + def test_binary_inequality(self): + m = ConcreteModel() + m.x = Var(domain=Binary) + m.y = Var(domain=Binary) + m.c = Constraint(expr=m.x == m.y) + m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + TransformationFactory('contrib.aggregate_vars').apply_to(m) + var_to_z = m._var_aggregator_info.var_to_z + z = var_to_z[m.x] + self.assertIs(var_to_z[m.y], z) + self.assertEqual(z.domain, Binary) + self.assertEqual(z.lb, 0) + self.assertEqual(z.ub, 1) + assertExpressionsEqual( + self, + m.o.expr, + 0.5 * z + z + ) + + def test_equality_different_domains(self): + m = ConcreteModel() + m.x = Var(domain=Reals, bounds=(1, 2)) + m.y = Var(domain=Binary) + m.c = Constraint(expr=m.x == m.y) + m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + TransformationFactory('contrib.aggregate_vars').apply_to(m) + var_to_z = m._var_aggregator_info.var_to_z + z = var_to_z[m.x] + self.assertIs(var_to_z[m.y], z) + self.assertEqual(z.lb, 1) + self.assertEqual(z.ub, 1) + self.assertEqual(z.domain, Binary) + assertExpressionsEqual( + self, + m.o.expr, + 0.5 * z + z + ) if __name__ == '__main__': unittest.main() From 127f8c68f9a97da4a34a5a52f61d0d08c6acf0bb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 13:56:16 -0600 Subject: [PATCH 0949/1178] black --- .../preprocessing/plugins/var_aggregator.py | 10 ++++++++-- .../preprocessing/tests/test_var_aggregator.py | 17 +++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/preprocessing/plugins/var_aggregator.py b/pyomo/contrib/preprocessing/plugins/var_aggregator.py index 5b714cf439d..3430d29de3a 100644 --- a/pyomo/contrib/preprocessing/plugins/var_aggregator.py +++ b/pyomo/contrib/preprocessing/plugins/var_aggregator.py @@ -13,8 +13,14 @@ from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.core.base import (Block, Constraint, VarList, Objective, Reals, - TransformationFactory) +from pyomo.core.base import ( + Block, + Constraint, + VarList, + Objective, + Reals, + TransformationFactory, +) from pyomo.core.expr import ExpressionReplacementVisitor from pyomo.core.expr.numvalue import value from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index ff1ea843d23..b0b672b76b0 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -219,7 +219,7 @@ def test_binary_inequality(self): m.x = Var(domain=Binary) m.y = Var(domain=Binary) m.c = Constraint(expr=m.x == m.y) - m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + m.o = Objective(expr=0.5 * m.x + m.y, sense=maximize) TransformationFactory('contrib.aggregate_vars').apply_to(m) var_to_z = m._var_aggregator_info.var_to_z z = var_to_z[m.x] @@ -227,18 +227,14 @@ def test_binary_inequality(self): self.assertEqual(z.domain, Binary) self.assertEqual(z.lb, 0) self.assertEqual(z.ub, 1) - assertExpressionsEqual( - self, - m.o.expr, - 0.5 * z + z - ) + assertExpressionsEqual(self, m.o.expr, 0.5 * z + z) def test_equality_different_domains(self): m = ConcreteModel() m.x = Var(domain=Reals, bounds=(1, 2)) m.y = Var(domain=Binary) m.c = Constraint(expr=m.x == m.y) - m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + m.o = Objective(expr=0.5 * m.x + m.y, sense=maximize) TransformationFactory('contrib.aggregate_vars').apply_to(m) var_to_z = m._var_aggregator_info.var_to_z z = var_to_z[m.x] @@ -246,11 +242,8 @@ def test_equality_different_domains(self): self.assertEqual(z.lb, 1) self.assertEqual(z.ub, 1) self.assertEqual(z.domain, Binary) - assertExpressionsEqual( - self, - m.o.expr, - 0.5 * z + z - ) + assertExpressionsEqual(self, m.o.expr, 0.5 * z + z) + if __name__ == '__main__': unittest.main() From 4f0c8d84ccb1f6cabbd90a0b0790db2e9d2311f5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 14:07:07 -0600 Subject: [PATCH 0950/1178] Simplify constant NL expressions in defined vars due to the presolver --- pyomo/repn/plugins/nl_writer.py | 125 +++++++++++++++++++-- pyomo/repn/tests/ampl/test_nlv2.py | 167 +++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ee5b65149ae..2de44b4f68b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -11,6 +11,8 @@ import ctypes import logging +import math +import operator import os from collections import deque, defaultdict, namedtuple from contextlib import nullcontext @@ -1835,7 +1837,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): fixed_vars.append(x) eliminated_cons.add(con_id) else: - return eliminated_cons, eliminated_vars + break for con_id, expr_info in comp_by_linear_var[_id]: # Note that if we were aggregating (i.e., _id was # from two_var), then one of these info's will be @@ -1888,6 +1890,32 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): elif a: expr_info.linear[x] = c * a + # Note: the ASL will (silently) produce incorrect answers if the + # nonlinear portion of a defined variable is a constant + # expression. This may now be the case if all the variables in + # the original nonlinear expression have been fixed. + for expr, info, _ in self.subexpression_cache.values(): + if not info.nonlinear: + continue + print(info.nonlinear) + nl, args = info.nonlinear + if not args or any(vid not in eliminated_vars for vid in args): + continue + # Ideally, we would just evaluate the named expression. + # However, there might be a linear portion of the named + # expression that still has free variables, and there is no + # guarantee that the user actually initialized the + # variables. So, we will fall back on parsing the (now + # constant) nonlinear fragment and evaluating it. + if info.linear is None: + info.linear = {} + info.nonlinear = None + info.const += _evaluate_constant_nl( + nl % tuple(template.const % eliminated_vars[i].const for i in args) + ) + + return eliminated_cons, eliminated_vars + def _record_named_expression_usage(self, named_exprs, src, comp_type): self.used_named_expressions.update(named_exprs) src = id(src) @@ -2263,6 +2291,40 @@ class text_nl_debug_template(object): _create_strict_inequality_map(vars()) +nl_operators = { + 0: (2, operator.add), + 2: (2, operator.mul), + 3: (2, operator.truediv), + 5: (2, operator.pow), + 15: (1, operator.abs), + 16: (1, operator.neg), + 54: (None, lambda *x: sum(x)), + 35: (3, lambda a, b, c: b if a else c), + 21: (2, operator.and_), + 22: (2, operator.lt), + 23: (2, operator.le), + 24: (2, operator.eq), + 43: (2, math.log), + 42: (2, math.log10), + 41: (2, math.sin), + 46: (2, math.cos), + 38: (2, math.tan), + 40: (2, math.sinh), + 45: (2, math.cosh), + 37: (2, math.tanh), + 51: (2, math.asin), + 53: (2, math.acos), + 49: (2, math.atan), + 44: (2, math.exp), + 39: (2, math.sqrt), + 50: (2, math.asinh), + 52: (2, math.acosh), + 47: (2, math.atanh), + 14: (2, math.ceil), + 13: (2, math.floor), +} + + def _strip_template_comments(vars_, base_): vars_['unary'] = {k: v[: v.find('\t#')] + '\n' for k, v in base_.unary.items()} for k, v in base_.__dict__.items(): @@ -2515,6 +2577,15 @@ def handle_named_expression_node(visitor, node, arg1): expression_source, ) + # As we will eventually need the compiled form of any nonlinear + # expression, we will go ahead and compile it here. We do not + # do the same for the linear component as we will only need the + # linear component compiled to a dict if we are emitting the + # original (linear + nonlinear) V line (which will not happen if + # the V line is part of a larger linear operator). + if repn.nonlinear.__class__ is list: + repn.compile_nonlinear_fragment(visitor) + if not visitor.use_named_exprs: return _GENERAL, repn.duplicate() @@ -2527,15 +2598,6 @@ def handle_named_expression_node(visitor, node, arg1): repn.nl = (visitor.template.var, (_id,)) if repn.nonlinear: - # As we will eventually need the compiled form of any nonlinear - # expression, we will go ahead and compile it here. We do not - # do the same for the linear component as we will only need the - # linear component compiled to a dict if we are emitting the - # original (linear + nonlinear) V line (which will not happen if - # the V line is part of a larger linear operator). - if repn.nonlinear.__class__ is list: - repn.compile_nonlinear_fragment(visitor) - if repn.linear: # If this expression has both linear and nonlinear # components, we will follow the ASL convention and break @@ -3016,3 +3078,46 @@ def finalizeResult(self, result): # self.active_expression_source = None return ans + + +def _evaluate_constant_nl(nl): + expr = nl.splitlines() + stack = [] + while expr: + line = expr.pop() + tokens = line.split() + # remove tokens after the first comment + for i, t in enumerate(tokens): + if t.startswith('#'): + tokens = tokens[:i] + break + if len(tokens) != 1: + # skip blank lines + if not tokens: + continue + raise DeveloperError( + f"Unsupported line format _evaluate_nl() (we expect each line " + f"to contain a single token): '{line}'" + ) + term = tokens[0] + # the "command" can be determined by the first character on the line + cmd = term[0] + # Note that we will unpack the line into the expected number of + # explicit arguments as a form of error checking + if cmd == 'n': + stack.append(float(term[1:])) + elif cmd == 'o': + # operator + nargs, fcn = nl_operators[int(term[1:])] + if nargs is None: + nargs = int(stack.pop()) + stack.append(fcn(*(stack.pop() for i in range(nargs)))) + elif cmd in '1234567890': + # this is either a single int (e.g., the nargs in a nary + # sum) or a string argument. Preserve it as-is until later + # when we know which we are expecting. + stack.append(term) + else: + raise DeveloperError(f"Unsupported NL operator in _evaluate_nl(): '{line}'") + assert len(stack) == 1 + return stack[0] diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index be72025edcd..54b0d93b52b 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2165,6 +2165,173 @@ def test_named_expressions(self): 0 0 1 0 2 0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_fixes_nl_defined_variables(self): + # This tests a workaround for a bug in the ASL where defined + # variables with nonstant expressions in the NL portion are not + # evaluated correctly. + m = ConcreteModel() + m.x = Var() + m.y = Var(bounds=(3, None)) + m.z = Var(bounds=(None, 3)) + m.e = Expression(expr=m.x + m.y * m.z + m.y**2 + 3 / m.z) + m.c1 = Constraint(expr=m.y * m.e + m.x >= 0) + m.c2 = Constraint(expr=m.y == m.z) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + export_defined_variables=True, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 0 0 0 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 1 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 1 0 #nonzeros in Jacobian, obj. gradient + 2 1 #max name lengths: constraints, variables + 0 0 0 2 0 #common exprs: b,c,o,c1,o1 +V1 0 1 #nl(e) +n19 +V2 1 1 #e +0 1 +v1 #nl(e) +C0 #c1 +o2 #* +n3 +v2 #e +x0 #initial guess +r #1 ranges (rhs's) +2 0 #c1 +b #1 bounds (on variables) +3 #x +k0 #intermediate Jacobian column lengths +J0 1 #c1 +0 1 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + export_defined_variables=False, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 0 0 0 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 1 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 1 0 #nonzeros in Jacobian, obj. gradient + 2 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #c1 +o2 #* +n3 +o0 #+ +v0 #x +o54 #sumlist +3 #(n) +o2 #* +n3 +n3 +o5 #^ +n3 +n2 +o3 #/ +n3 +n3 +x0 #initial guess +r #1 ranges (rhs's) +2 0 #c1 +b #1 bounds (on variables) +3 #x +k0 #intermediate Jacobian column lengths +J0 1 #c1 +0 1 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=False, + export_defined_variables=True, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 3 2 0 0 1 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 3 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 5 0 #nonzeros in Jacobian, obj. gradient + 2 1 #max name lengths: constraints, variables + 0 0 0 2 0 #common exprs: b,c,o,c1,o1 +V3 0 1 #nl(e) +o54 #sumlist +3 #(n) + o2 #* + v0 #y +v2 #z +o5 #^ +v0 #y +n2 +o3 #/ +n3 +v2 #z +V4 1 1 #e +1 1 +v3 #nl(e) +C0 #c1 +o2 #* +v0 #y +v4 #e +C1 #c2 +n0 +x0 #initial guess +r #2 ranges (rhs's) +2 0 #c1 +4 0 #c2 +b #3 bounds (on variables) +2 3 #y +3 #x +1 3 #z +k2 #intermediate Jacobian column lengths +2 +3 +J0 3 #c1 +0 0 +1 1 +2 0 +J1 2 #c2 +0 1 +2 -1 """, OUT.getvalue(), ) From afa542f8e565b43a05b73e62398d29e7f2753f6b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 19 Apr 2024 09:06:00 -0600 Subject: [PATCH 0951/1178] Improve comments/error messages --- pyomo/repn/plugins/nl_writer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index f91f88ff5cf..fc8b8b10309 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -3102,8 +3102,8 @@ def _evaluate_constant_nl(nl): if not tokens: continue raise DeveloperError( - f"Unsupported line format _evaluate_nl() (we expect each line " - f"to contain a single token): '{line}'" + f"Unsupported line format _evaluate_constant_nl() " + f"(we expect each line to contain a single token): '{line}'" ) term = tokens[0] # the "command" can be determined by the first character on the line @@ -3111,6 +3111,7 @@ def _evaluate_constant_nl(nl): # Note that we will unpack the line into the expected number of # explicit arguments as a form of error checking if cmd == 'n': + # numeric constant stack.append(float(term[1:])) elif cmd == 'o': # operator @@ -3124,6 +3125,8 @@ def _evaluate_constant_nl(nl): # when we know which we are expecting. stack.append(term) else: - raise DeveloperError(f"Unsupported NL operator in _evaluate_nl(): '{line}'") + raise DeveloperError( + f"Unsupported NL operator in _evaluate_constant_nl(): '{line}'" + ) assert len(stack) == 1 return stack[0] From 1a8ab9621c0252fedca294e3e599f363253ce158 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 19 Apr 2024 09:06:53 -0600 Subject: [PATCH 0952/1178] Fix typo in baseline --- pyomo/repn/tests/ampl/test_nlv2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 64830d36921..f2378e56b4c 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2435,8 +2435,8 @@ def test_presolve_fixes_nl_defined_variables(self): V3 0 1 #nl(e) o54 #sumlist 3 #(n) - o2 #* - v0 #y +o2 #* +v0 #y v2 #z o5 #^ v0 #y From ebe0159071ef2a60a6e71dd3131913907e5aa64b Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 19 Apr 2024 23:15:33 -0600 Subject: [PATCH 0953/1178] start adding an inventory of code to readme --- pyomo/contrib/pynumero/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index 0d165dbc39c..d4b6344ec76 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -71,3 +71,34 @@ Prerequisites - cmake - a C/C++ compiler - MA57 library or COIN-HSL Full + +Code organization +================= + +PyNumero was initially designed around three core components: linear solver +interfaces, an interface for function and derivative callbacks, and block +vector and matrix classes. Since then, it has incorporated additional +functionality in an ad-hoc manner. The following is a rough overview of +PyNumero, by directory: + +`linalg` +-------- + +Python interfaces to linear solvers. This is core functionality. + +`interfaces` +------------ + +- Classes that define and implement an API for function and derivative callbacks +required by nonlinear optimization solvers, e.g. `nlp.py` and `pyomo_nlp.py` +- Various wrappers around these NLP classes to support "hybrid" implementations, +e.g. `PyomoNLPWithGreyBoxBlocks` +- The `ExternalGreyBoxBlock` Pyomo modeling component and +`ExternalGreyBoxModel` API +- The `ExternalPyomoModel` implementation of `ExternalGreyBoxModel`, which allows +definition of an external grey box via an implicit function +- The `CyIpoptNLP` class, which wraps an object implementing the NLP API in +the interface required by CyIpopt + +`src` +----- From d4dcb0e81bbc20dc6597fc76b1400b81c02cd100 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 19 Apr 2024 23:16:14 -0600 Subject: [PATCH 0954/1178] add a note about backward compatibility to pynumero doc --- .../pynumero/backward_compatibility.rst | 14 ++++++++++++++ .../contributed_packages/pynumero/index.rst | 1 + 2 files changed, 15 insertions(+) create mode 100644 doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst b/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst new file mode 100644 index 00000000000..036a00bee62 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst @@ -0,0 +1,14 @@ +Backward Compatibility +====================== + +While PyNumero is a third-party contribution to Pyomo, we intend to maintain +the stability of its core functionality. The core functionality of PyNumero +consists of: + +1. The ``NLP`` API and ``PyomoNLP`` implementation of this API +2. HSL and MUMPS linear solver interfaces +3. ``BlockVector`` and ``BlockMatrix`` classes +4. CyIpopt and SciPy solver interfaces + +Other parts of PyNumero, such as ``ExternalGreyBoxBlock`` and +``ImplicitFunctionSolver``, are experimental and subject to change without notice. diff --git a/doc/OnlineDocs/contributed_packages/pynumero/index.rst b/doc/OnlineDocs/contributed_packages/pynumero/index.rst index 6ff8b29f812..711bb83eb3b 100644 --- a/doc/OnlineDocs/contributed_packages/pynumero/index.rst +++ b/doc/OnlineDocs/contributed_packages/pynumero/index.rst @@ -13,6 +13,7 @@ PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). installation.rst tutorial.rst api.rst + backward_compatibility.rst Developers From 102223f541f82b38085c15489d2884744dfbb8f8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Apr 2024 15:25:46 -0600 Subject: [PATCH 0955/1178] NFC: clarify NamedExpressionData docstring --- pyomo/core/base/expression.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 5638e48ea8b..013c388e6e5 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -37,11 +37,14 @@ class NamedExpressionData(numeric_expr.NumericValue): - """ - An object that defines a named expression. + """An object that defines a generic "named expression". + + This is the base class for both :py:class:`ExpressionData` and + :py:class:`ObjectiveData`. Public Class Attributes expr The expression owned by this data. + """ # Note: derived classes are expected to declare the _args_ slot From a5c79317bfa68a91e5081682e36873559acea5ab Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Apr 2024 15:27:21 -0600 Subject: [PATCH 0956/1178] Remove debugging --- pyomo/repn/plugins/nl_writer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index fc8b8b10309..e4a2cea0bad 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1903,7 +1903,6 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): for expr, info, _ in self.subexpression_cache.values(): if not info.nonlinear: continue - print(info.nonlinear) nl, args = info.nonlinear if not args or any(vid not in eliminated_vars for vid in args): continue From 8390ce4321934eae0288dd8fe756fe0cbfaea5e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Apr 2024 15:30:21 -0600 Subject: [PATCH 0957/1178] Fix the arg count for unary operators --- pyomo/repn/plugins/nl_writer.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e4a2cea0bad..d4e7485cce4 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2309,24 +2309,24 @@ class text_nl_debug_template(object): 22: (2, operator.lt), 23: (2, operator.le), 24: (2, operator.eq), - 43: (2, math.log), - 42: (2, math.log10), - 41: (2, math.sin), - 46: (2, math.cos), - 38: (2, math.tan), - 40: (2, math.sinh), - 45: (2, math.cosh), - 37: (2, math.tanh), - 51: (2, math.asin), - 53: (2, math.acos), - 49: (2, math.atan), - 44: (2, math.exp), - 39: (2, math.sqrt), - 50: (2, math.asinh), - 52: (2, math.acosh), - 47: (2, math.atanh), - 14: (2, math.ceil), - 13: (2, math.floor), + 43: (1, math.log), + 42: (1, math.log10), + 41: (1, math.sin), + 46: (1, math.cos), + 38: (1, math.tan), + 40: (1, math.sinh), + 45: (1, math.cosh), + 37: (1, math.tanh), + 51: (1, math.asin), + 53: (1, math.acos), + 49: (1, math.atan), + 44: (1, math.exp), + 39: (1, math.sqrt), + 50: (1, math.asinh), + 52: (1, math.acosh), + 47: (1, math.atanh), + 14: (1, math.ceil), + 13: (1, math.floor), } From cc2803483b05f6c9348b303aac4105d181346e49 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 23 Apr 2024 21:14:32 -0400 Subject: [PATCH 0958/1178] Fixed error about if values being ambiguous --- pyomo/contrib/doe/measurements.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 5a3c44a76e4..11b84b4231b 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -93,17 +93,17 @@ def add_variables( upper_bounds, ) - if values: + if values is not None: # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) # if a scalar (int or float) is given, set it as the lower bound for all variables - if lower_bounds: + if lower_bounds is not None: if type(lower_bounds) in [int, float]: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) - if upper_bounds: + if upper_bounds is not None: if type(upper_bounds) in [int, float]: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) @@ -177,20 +177,20 @@ def _check_valid_input( raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if values and len(values) != len_indices: + if values is not None and len(values) != len_indices: raise ValueError("Values is of different length with indices.") if ( - lower_bounds - and type(lower_bounds) == list - and len(lower_bounds) != len_indices + lower_bounds is not None # ensure not None + and type(lower_bounds) == list # ensure list + and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds - and type(upper_bounds) == list - and len(upper_bounds) != len_indices + upper_bounds is not None # ensure None + and type(upper_bounds) == list # ensure list + and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") From 6d421a428c46a7621fb1f356406d392b3bc0e9e3 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 23 Apr 2024 21:31:39 -0400 Subject: [PATCH 0959/1178] Added exception to prevent cryptic error messages later. --- pyomo/contrib/doe/doe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d2ba2f277d6..eba22b954cf 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -101,6 +101,8 @@ def __init__( """ # parameters + if type(param_init) != dict: + raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name self.design_name = design_vars.variable_names From f1d480fe970472acc1919f4b32c8f9808bc2a179 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 24 Apr 2024 09:55:31 -0600 Subject: [PATCH 0960/1178] Fixing a bug where a division by 0 wasn't trapped and propogated as an invalid number --- pyomo/repn/linear.py | 2 +- pyomo/repn/tests/test_linear.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index ba08c7ef245..6d084067511 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -292,7 +292,7 @@ def _handle_division_constant_constant(visitor, node, arg1, arg2): def _handle_division_ANY_constant(visitor, node, arg1, arg2): - arg1[1].multiplier /= arg2[1] + arg1[1].multiplier = apply_node_operation(node, (arg1[1].multiplier, arg2[1])) return arg1 diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 0fd428fd8ee..861fecc7888 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1436,6 +1436,22 @@ def test_errors_propagate_nan(self): m.z = Var() m.y.fix(1) + expr = (m.x + 1) / m.p + cfg = VisitorConfig() + with LoggingIntercept() as LOG: + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual( + LOG.getvalue(), + "Exception encountered evaluating expression 'div(1, 0)'\n" + "\tmessage: division by zero\n" + "\texpression: (x + 1)/p\n", + ) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(len(repn.linear), 1) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y cfg = VisitorConfig() with LoggingIntercept() as LOG: From 910cf2d1247f0d75e80b6bd776236e8e05ae6beb Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Wed, 24 Apr 2024 21:04:11 -0400 Subject: [PATCH 0961/1178] Added units. --- pyomo/contrib/doe/doe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index eba22b954cf..8c83e3145ce 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -240,12 +240,12 @@ def stochastic_program( if self.optimize: analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return analysis_square, analysis_optimize else: dT = sp_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return analysis_square def _compute_stochastic_program(self, m, optimize_option): @@ -389,7 +389,7 @@ def compute_FIM( FIM_analysis = self._direct_kaug() dT = square_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return FIM_analysis From 5df54523e46be0e66aac827e21286a9977592011 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Wed, 24 Apr 2024 21:52:51 -0400 Subject: [PATCH 0962/1178] Switched to isinstance --- pyomo/contrib/doe/doe.py | 6 +++--- pyomo/contrib/doe/measurements.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 8c83e3145ce..b55f9aac0ac 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -38,7 +38,7 @@ from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp from pyomo.contrib.doe.scenario import ScenarioGenerator, FiniteDifferenceStep from pyomo.contrib.doe.result import FisherResults, GridSearchResult - +import collections class CalculationMode(Enum): sequential_finite = "sequential_finite" @@ -101,7 +101,7 @@ def __init__( """ # parameters - if type(param_init) != dict: + if not isinstance(param_init, collections.Mapping): raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name @@ -777,7 +777,7 @@ def run_grid_search( # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): # if the element is a list, all design variables in this list share the same values - if type(names) is list or type(names) is tuple: + if isinstance(names, collections.Sequence): for n in names: design_iter[n] = list(design_set_iter)[i] else: diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 11b84b4231b..aa196ec9a49 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -26,6 +26,7 @@ # ___________________________________________________________________________ import itertools +import collections class VariablesWithIndices: @@ -171,7 +172,7 @@ def _check_valid_input( """ Check if the measurement information provided are valid to use. """ - assert type(var_name) is str, "var_name should be a string." + assert isinstance(var_name, str), "var_name should be a string." if time_index_position not in indices: raise ValueError("time index cannot be found in indices.") @@ -182,14 +183,14 @@ def _check_valid_input( if ( lower_bounds is not None # ensure not None - and type(lower_bounds) == list # ensure list + and isinstance(lower_bounds, collections.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( upper_bounds is not None # ensure None - and type(upper_bounds) == list # ensure list + and isinstance(upper_bounds, collections.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") From ea9d226f368959e3e028a4bd9a4a7bb77ea29938 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 24 Apr 2024 22:08:44 -0600 Subject: [PATCH 0963/1178] Skip black 24.4.1 due to a bug in the parser --- .github/workflows/test_branches.yml | 3 ++- .github/workflows/test_pr_and_main.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 1885f6a00e2..75db5d66431 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -40,7 +40,8 @@ jobs: python-version: '3.10' - name: Black Formatting Check run: | - pip install black + # Note v24.4.1 fails due to a bug in the parser + pip install 'black!=24.4.1' black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py - name: Spell Check uses: crate-ci/typos@master diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 619a5e695e2..eb059a7ef82 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -43,7 +43,8 @@ jobs: python-version: '3.10' - name: Black Formatting Check run: | - pip install black + # Note v24.4.1 fails due to a bug in the parser + pip install 'black!=24.4.1' black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py - name: Spell Check uses: crate-ci/typos@master From 98ee3ec8ad70b49f25a8aade1453c1d3be58ac8a Mon Sep 17 00:00:00 2001 From: robbybp Date: Wed, 24 Apr 2024 22:16:20 -0600 Subject: [PATCH 0964/1178] add more description to readme --- pyomo/contrib/pynumero/README.md | 43 ++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index d4b6344ec76..d68c055e97c 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -78,8 +78,13 @@ Code organization PyNumero was initially designed around three core components: linear solver interfaces, an interface for function and derivative callbacks, and block vector and matrix classes. Since then, it has incorporated additional -functionality in an ad-hoc manner. The following is a rough overview of -PyNumero, by directory: +functionality in an ad-hoc manner. The original "core functionality" of +PyNumero, as well as the solver interfaces accessible through +`SolverFactory`, should be considered stable and will only change after +appropriate deprecation warnings. Other functionality should be considered +experimental and subject to change without warning. + +The following is a rough overview of PyNumero, by directory: `linalg` -------- @@ -100,5 +105,39 @@ definition of an external grey box via an implicit function - The `CyIpoptNLP` class, which wraps an object implementing the NLP API in the interface required by CyIpopt +Of the above, only `PyomoNLP` and the `NLP` base class should be considered core +functionality. + `src` ----- + +C++ interfaces to ASL, MA27, and MA57. The ASL and MA27 interfaces are +core functionality. + +`sparse` +-------- + +Block vector and block matrix classes, including MPI variations. +These are core functionality. + +`algorithms` +------------ + +Originally intended to hold various useful algorithms implemented +on NLP objects rather than Pyomo models. Any files added here should +be considered experimental. + +`algorithms/solvers` +-------------------- + +Interfaces to Python solvers using the NLP API defined in `interfaces`. +Only the solvers accessible through `SolverFactory`, e.g. `PyomoCyIpoptSolver` +and `PyomoFsolveSolver`, should be considered core functionality. + +`examples` +---------- + +The examples demonstrated in `nlp_interface.py`, `nlp_interface_2.py1`, +`feasibility.py`, `mumps_example.py`, `sensitivity.py`, `sqp.py`, +`parallel_matvec.py`, and `parallel_vector_ops.py` are stable. All other +examples should be considered experimental. From 5689e7406574d5baa8d80c38f91264f7d9fc59ed Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 25 Apr 2024 09:33:06 -0600 Subject: [PATCH 0965/1178] add note that location of pyomo solvers is subject to change --- pyomo/contrib/pynumero/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index d68c055e97c..f881e400d51 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -133,6 +133,8 @@ be considered experimental. Interfaces to Python solvers using the NLP API defined in `interfaces`. Only the solvers accessible through `SolverFactory`, e.g. `PyomoCyIpoptSolver` and `PyomoFsolveSolver`, should be considered core functionality. +The supported way to access these solvers is via `SolverFactory`. *The locations +of the underlying solver objects are subject to change without warning.* `examples` ---------- From a0bc0891f900993692c65c88c2c0e9aacc435d88 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:30:23 -0600 Subject: [PATCH 0966/1178] Incorporate suggestion on wording Co-authored-by: Bethany Nicholson --- doc/OnlineDocs/contribution_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 6054a8d2ba9..b98dcc3d014 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -93,7 +93,7 @@ active development may be marked 'stale' and closed. .. note:: Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to - keep our backlog as available as possible. Please make use of the provided + reduce our CI backlog. Please make use of the provided branch test suite for evaluating / testing draft functionality. Python Version Support From 312f5722b1a703413133c887b415a0d249d1e032 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:46:59 -0400 Subject: [PATCH 0967/1178] Initialization improvements and degree of freedom fixes are important! --- pyomo/contrib/doe/doe.py | 116 +++++++++++++++++++++++++++--- pyomo/contrib/doe/measurements.py | 6 +- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b55f9aac0ac..cac9bfd9271 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -38,7 +38,9 @@ from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp from pyomo.contrib.doe.scenario import ScenarioGenerator, FiniteDifferenceStep from pyomo.contrib.doe.result import FisherResults, GridSearchResult -import collections +import collections.abc + +import inspect class CalculationMode(Enum): sequential_finite = "sequential_finite" @@ -101,7 +103,7 @@ def __init__( """ # parameters - if not isinstance(param_init, collections.Mapping): + if not isinstance(param_init, collections.abc.Mapping): raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name @@ -238,6 +240,10 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: + # set max_iter to 0 to debug the initialization + # self.solver.options["max_iter"] = 0 + # self.solver.options["bound_push"] = 1e-10 + analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) @@ -588,13 +594,34 @@ def _create_block(self): # Set for block/scenarios mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) + + # Determine if create_model takes theta as an optional input + # print(inspect.getfullargspec(self.create_model)) + pass_theta_to_initialize= ('theta' in inspect.getfullargspec(self.create_model).args) + #print("pass_theta_to_initialize =", pass_theta_to_initialize) # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) + # Fix parameter values in the copy of the stage1 model (if they exist) + for par in self.param: + cuid = pyo.ComponentUID(par) + var = cuid.find_component_on(mod) + if var is not None: + # Fix the parameter value + # Otherwise, the parameter does not exist on the stage 1 model + var.fix(self.param[par]) + def block_build(b, s): # create block scenarios - self.create_model(mod=b, model_option=ModelOptionLib.stage2) + # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model + + if pass_theta_to_initialize: + theta_initialize = self.scenario_data.scenario[s] + #print("Initializing with theta=", theta_initialize) + self.create_model(mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize) + else: + self.create_model(mod=b, model_option=ModelOptionLib.stage2) # fix parameter values to perturbed values for par in self.param: @@ -605,7 +632,7 @@ def block_build(b, s): mod.block = pyo.Block(mod.scenario, rule=block_build) # discretize the model - if self.discretize_model: + if self.discretize_model is not None: mod = self.discretize_model(mod) # force design variables in blocks to be equal to global design values @@ -777,7 +804,7 @@ def run_grid_search( # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): # if the element is a list, all design variables in this list share the same values - if isinstance(names, collections.Sequence): + if isinstance(names, collections.abc.Sequence): for n in names: design_iter[n] = list(design_set_iter)[i] else: @@ -881,20 +908,39 @@ def identity_matrix(m, i, j): else: return 0 + if self.jac_initial is not None: + dict_jac_initialize = {} + for i, bu in enumerate(model.regression_parameters): + for j, un in enumerate(model.measured_variables): + if isinstance(self.jac_initial, dict): + # Jacobian is a dictionary of arrays or lists where the key is the regression parameter name + dict_jac_initialize[(bu, un)] = self.jac_initial[bu][j] + elif isinstance(self.jac_initial, np.ndarray): + # Jacobian is a numpy array, rows are regression parameters, columns are measured variables + dict_jac_initialize[(bu, un)] = self.jac_initial[i][j] + + def initialize_jac(m, i, j): + if self.jac_initial is not None: + return dict_jac_initialize[(i, j)] + else: + return 0.1 + model.sensitivity_jacobian = pyo.Var( - model.regression_parameters, model.measured_variables, initialize=0.1 + model.regression_parameters, model.measured_variables, initialize=initialize_jac ) - if self.fim_initial: + if self.fim_initial is not None: dict_fim_initialize = {} for i, bu in enumerate(model.regression_parameters): for j, un in enumerate(model.regression_parameters): dict_fim_initialize[(bu, un)] = self.fim_initial[i][j] + + #print(dict_fim_initialize) def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] - if self.fim_initial: + if self.fim_initial is not None: model.fim = pyo.Var( model.regression_parameters, model.regression_parameters, @@ -1013,6 +1059,32 @@ def fim_rule(m, p, q): return model def _add_objective(self, m): + + ### Initialize the Cholesky decomposition matrix + if self.Cholesky_option: + + # Assemble the FIM matrix + fim = np.zeros((len(self.param), len(self.param))) + for i, bu in enumerate(m.regression_parameters): + for j, un in enumerate(m.regression_parameters): + fim[i][j] = m.fim[bu, un].value + + # Calculate the eigenvalues of the FIM matrix + eig = np.linalg.eigvals(fim) + + # If the smallest eigenvalue is (pratcially) negative, add a diagonal matrix to make it positive definite + small_number = 1E-10 + if min(eig) < small_number: + fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) + + # Compute the Cholesky decomposition of the FIM matrix + L = np.linalg.cholesky(fim) + + # Initialize the Cholesky matrix + for i, c in enumerate(m.regression_parameters): + for j, d in enumerate(m.regression_parameters): + m.L_ele[c, d].value = L[i, j] + def cholesky_imp(m, c, d): """ Calculate Cholesky L matrix using algebraic constraints @@ -1103,14 +1175,20 @@ def _fix_design(self, m, design_val, fix_opt=True, optimize_option=None): m: model """ for name in self.design_name: + # Loop over design variables + # Get Pyomo variable object cuid = pyo.ComponentUID(name) var = cuid.find_component_on(m) if fix_opt: + # If fix_opt is True, fix the design variable var.fix(design_val[name]) else: + # Otherwise check optimize_option if optimize_option is None: + # If optimize_option is None, unfix all design variables var.unfix() else: + # Otherwise, unfix only the design variables listed in optimize_option with value True if optimize_option[name]: var.unfix() return m @@ -1126,7 +1204,7 @@ def _get_default_ipopt_solver(self): def _solve_doe(self, m, fix=False, opt_option=None): """Solve DOE model. If it's a square problem, fix design variable and solve. - Else, fix design variable and solve square problem firstly, then unfix them and solve the optimization problem + Else, fix design variable and solve square problem first, then unfix them and solve the optimization problem Parameters ---------- @@ -1140,14 +1218,30 @@ def _solve_doe(self, m, fix=False, opt_option=None): ------- solver_results: solver results """ - ### Solve square problem + # if fix = False, solve the optimization problem + # if fix = True, solve the square problem + + # either fix or unfix the design variables mod = self._fix_design( m, self.design_values, fix_opt=fix, optimize_option=opt_option ) + ''' + # This is for initialization diagnostics + # Remove before merging the PR + if not fix: + # halt at initial point + self.solver.options['max_iter'] = 0 + self.solver.options['bound_push'] = 1E-10 + else: + # resort to defaults + self.solver.options['max_iter'] = 3000 + self.solver.options['bound_push'] = 0.01 + ''' + # if user gives solver, use this solver. if not, use default IPOPT solver solver_result = self.solver.solve(mod, tee=self.tee_opt) - + return solver_result def _sgn(self, p): diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index aa196ec9a49..ae5b3519498 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -26,7 +26,7 @@ # ___________________________________________________________________________ import itertools -import collections +import collections.abc class VariablesWithIndices: @@ -183,14 +183,14 @@ def _check_valid_input( if ( lower_bounds is not None # ensure not None - and isinstance(lower_bounds, collections.Sequence) # ensure list-like + and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( upper_bounds is not None # ensure None - and isinstance(upper_bounds, collections.Sequence) # ensure list-like + and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") From 78c8558d3c0bc0beca6683ffe6b124223e501f6d Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:53:56 -0400 Subject: [PATCH 0968/1178] Updated one type check and removed extra code from debugging. --- pyomo/contrib/doe/doe.py | 29 +++++++---------------------- pyomo/contrib/doe/measurements.py | 6 +++--- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index cac9bfd9271..d10fc1b7fcc 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -230,6 +230,7 @@ def stochastic_program( # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled self.fim_scale_constant_value = self.scale_constant_value**2 + # Start timer sp_timer = TicTocTimer() sp_timer.tic(msg=None) @@ -240,18 +241,17 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: - # set max_iter to 0 to debug the initialization - # self.solver.options["max_iter"] = 0 - # self.solver.options["bound_push"] = 1e-10 - + # If set to optimize, solve the optimization problem (with degrees of freedom) analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) + # Return both square problem and optimization problem results return analysis_square, analysis_optimize else: dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) + # Return only square problem results return analysis_square def _compute_stochastic_program(self, m, optimize_option): @@ -596,9 +596,7 @@ def _create_block(self): mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) # Determine if create_model takes theta as an optional input - # print(inspect.getfullargspec(self.create_model)) pass_theta_to_initialize= ('theta' in inspect.getfullargspec(self.create_model).args) - #print("pass_theta_to_initialize =", pass_theta_to_initialize) # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) @@ -617,10 +615,12 @@ def block_build(b, s): # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block theta_initialize = self.scenario_data.scenario[s] - #print("Initializing with theta=", theta_initialize) + # Add model on block with theta values self.create_model(mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize) else: + # Otherwise add model on block without theta values self.create_model(mod=b, model_option=ModelOptionLib.stage2) # fix parameter values to perturbed values @@ -934,8 +934,6 @@ def initialize_jac(m, i, j): for i, bu in enumerate(model.regression_parameters): for j, un in enumerate(model.regression_parameters): dict_fim_initialize[(bu, un)] = self.fim_initial[i][j] - - #print(dict_fim_initialize) def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] @@ -1226,19 +1224,6 @@ def _solve_doe(self, m, fix=False, opt_option=None): m, self.design_values, fix_opt=fix, optimize_option=opt_option ) - ''' - # This is for initialization diagnostics - # Remove before merging the PR - if not fix: - # halt at initial point - self.solver.options['max_iter'] = 0 - self.solver.options['bound_push'] = 1E-10 - else: - # resort to defaults - self.solver.options['max_iter'] = 3000 - self.solver.options['bound_push'] = 0.01 - ''' - # if user gives solver, use this solver. if not, use default IPOPT solver solver_result = self.solver.solve(mod, tee=self.tee_opt) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index ae5b3519498..dcaac1f14fd 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -27,7 +27,7 @@ import itertools import collections.abc - +from pyomo.common.numeric_types import native_numeric_types class VariablesWithIndices: def __init__(self): @@ -100,12 +100,12 @@ def add_variables( # if a scalar (int or float) is given, set it as the lower bound for all variables if lower_bounds is not None: - if type(lower_bounds) in [int, float]: + if type(lower_bounds) in native_numeric_types: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) if upper_bounds is not None: - if type(upper_bounds) in [int, float]: + if type(upper_bounds) in native_numeric_types: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) From 845fc3fdcae84180cd7f777887a771f55074e2bf Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:58:33 -0400 Subject: [PATCH 0969/1178] Ran black --- pyomo/contrib/doe/doe.py | 27 +++++++++++++++++---------- pyomo/contrib/doe/measurements.py | 13 +++++++------ pyomo/contrib/doe/result.py | 2 +- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d10fc1b7fcc..ed5e81027dd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -42,6 +42,7 @@ import inspect + class CalculationMode(Enum): sequential_finite = "sequential_finite" direct_kaug = "direct_kaug" @@ -228,7 +229,7 @@ def stochastic_program( # calculate how much the FIM element is scaled by a constant number # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled - self.fim_scale_constant_value = self.scale_constant_value**2 + self.fim_scale_constant_value = self.scale_constant_value ** 2 # Start timer sp_timer = TicTocTimer() @@ -241,7 +242,7 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: - # If set to optimize, solve the optimization problem (with degrees of freedom) + # If set to optimize, solve the optimization problem (with degrees of freedom) analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) @@ -382,7 +383,7 @@ def compute_FIM( # calculate how much the FIM element is scaled by a constant number # As FIM~Jacobian.T@Jacobian, FIM is scaled twice the number the Q is scaled - self.fim_scale_constant_value = self.scale_constant_value**2 + self.fim_scale_constant_value = self.scale_constant_value ** 2 square_timer = TicTocTimer() square_timer.tic(msg=None) @@ -594,9 +595,11 @@ def _create_block(self): # Set for block/scenarios mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) - + # Determine if create_model takes theta as an optional input - pass_theta_to_initialize= ('theta' in inspect.getfullargspec(self.create_model).args) + pass_theta_to_initialize = ( + 'theta' in inspect.getfullargspec(self.create_model).args + ) # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) @@ -618,7 +621,9 @@ def block_build(b, s): # Grab the values of theta for this scenario/block theta_initialize = self.scenario_data.scenario[s] # Add model on block with theta values - self.create_model(mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize) + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize + ) else: # Otherwise add model on block without theta values self.create_model(mod=b, model_option=ModelOptionLib.stage2) @@ -773,7 +778,7 @@ def run_grid_search( self.store_optimality_as_csv = store_optimality_as_csv # calculate how much the FIM element is scaled - self.fim_scale_constant_value = scale_constant_value**2 + self.fim_scale_constant_value = scale_constant_value ** 2 # to store all FIM results result_combine = {} @@ -926,7 +931,9 @@ def initialize_jac(m, i, j): return 0.1 model.sensitivity_jacobian = pyo.Var( - model.regression_parameters, model.measured_variables, initialize=initialize_jac + model.regression_parameters, + model.measured_variables, + initialize=initialize_jac, ) if self.fim_initial is not None: @@ -1071,7 +1078,7 @@ def _add_objective(self, m): eig = np.linalg.eigvals(fim) # If the smallest eigenvalue is (pratcially) negative, add a diagonal matrix to make it positive definite - small_number = 1E-10 + small_number = 1e-10 if min(eig) < small_number: fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) @@ -1226,7 +1233,7 @@ def _solve_doe(self, m, fix=False, opt_option=None): # if user gives solver, use this solver. if not, use default IPOPT solver solver_result = self.solver.solve(mod, tee=self.tee_opt) - + return solver_result def _sgn(self, p): diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index dcaac1f14fd..fd3962f7888 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -29,6 +29,7 @@ import collections.abc from pyomo.common.numeric_types import native_numeric_types + class VariablesWithIndices: def __init__(self): """This class provides utility methods for DesignVariables and MeasurementVariables to create @@ -182,16 +183,16 @@ def _check_valid_input( raise ValueError("Values is of different length with indices.") if ( - lower_bounds is not None # ensure not None - and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like - and len(lower_bounds) != len_indices # ensure same length + lower_bounds is not None # ensure not None + and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like + and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds is not None # ensure None - and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like - and len(upper_bounds) != len_indices # ensure same length + upper_bounds is not None # ensure None + and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like + and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 1593214c30a..8f98e74f159 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -81,7 +81,7 @@ def __init__( self.prior_FIM = prior_FIM self.store_FIM = store_FIM self.scale_constant_value = scale_constant_value - self.fim_scale_constant_value = scale_constant_value**2 + self.fim_scale_constant_value = scale_constant_value ** 2 self.max_condition_number = max_condition_number self.logger = logging.getLogger(__name__) self.logger.setLevel(level=logging.WARN) From 5c7cab5bb4629f2dc5ed6dde08d006c78b3735bf Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:59:45 -0400 Subject: [PATCH 0970/1178] Reran black. --- pyomo/contrib/doe/doe.py | 6 +++--- pyomo/contrib/doe/result.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ed5e81027dd..0d2c7c2f982 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -229,7 +229,7 @@ def stochastic_program( # calculate how much the FIM element is scaled by a constant number # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled - self.fim_scale_constant_value = self.scale_constant_value ** 2 + self.fim_scale_constant_value = self.scale_constant_value**2 # Start timer sp_timer = TicTocTimer() @@ -383,7 +383,7 @@ def compute_FIM( # calculate how much the FIM element is scaled by a constant number # As FIM~Jacobian.T@Jacobian, FIM is scaled twice the number the Q is scaled - self.fim_scale_constant_value = self.scale_constant_value ** 2 + self.fim_scale_constant_value = self.scale_constant_value**2 square_timer = TicTocTimer() square_timer.tic(msg=None) @@ -778,7 +778,7 @@ def run_grid_search( self.store_optimality_as_csv = store_optimality_as_csv # calculate how much the FIM element is scaled - self.fim_scale_constant_value = scale_constant_value ** 2 + self.fim_scale_constant_value = scale_constant_value**2 # to store all FIM results result_combine = {} diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 8f98e74f159..1593214c30a 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -81,7 +81,7 @@ def __init__( self.prior_FIM = prior_FIM self.store_FIM = store_FIM self.scale_constant_value = scale_constant_value - self.fim_scale_constant_value = scale_constant_value ** 2 + self.fim_scale_constant_value = scale_constant_value**2 self.max_condition_number = max_condition_number self.logger = logging.getLogger(__name__) self.logger.setLevel(level=logging.WARN) From a8a5450fc5021b1c30daafcdb282f0e936a8d9ad Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 22:06:38 -0400 Subject: [PATCH 0971/1178] Added more comments. --- pyomo/contrib/doe/doe.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0d2c7c2f982..28dad1f20c7 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -913,6 +913,9 @@ def identity_matrix(m, i, j): else: return 0 + ### Initialize the Jacobian if provided by the user + + # If the user provides an initial Jacobian, convert it to a dictionary if self.jac_initial is not None: dict_jac_initialize = {} for i, bu in enumerate(model.regression_parameters): @@ -924,9 +927,12 @@ def identity_matrix(m, i, j): # Jacobian is a numpy array, rows are regression parameters, columns are measured variables dict_jac_initialize[(bu, un)] = self.jac_initial[i][j] + # Initialize the Jacobian matrix def initialize_jac(m, i, j): + # If provided by the user, use the values now stored in the dictionary if self.jac_initial is not None: return dict_jac_initialize[(i, j)] + # Otherwise initialize to 0.1 (which is an arbitrary non-zero value) else: return 0.1 From 1bac09e842eaaeeeaef8191e30e500f2e311fefb Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 22:21:24 -0400 Subject: [PATCH 0972/1178] Reran black --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 28dad1f20c7..ab9a5ad9f85 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -914,7 +914,7 @@ def identity_matrix(m, i, j): return 0 ### Initialize the Jacobian if provided by the user - + # If the user provides an initial Jacobian, convert it to a dictionary if self.jac_initial is not None: dict_jac_initialize = {} From f4e989fc390cd08ec068dcdcb2b1826683bbb88d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:37:24 -0600 Subject: [PATCH 0973/1178] Add fileutils patch so find_library returns absolute path on Linux --- pyomo/common/fileutils.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 2cade36154d..80fd47a6377 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -38,6 +38,7 @@ import os import platform import importlib.util +import subprocess import sys from . import envvar @@ -375,9 +376,25 @@ def find_library(libname, cwd=True, include_PATH=True, pathlist=None): if libname_base.startswith('lib') and _system() != 'windows': libname_base = libname_base[3:] if ext.lower().startswith(('.so', '.dll', '.dylib')): - return ctypes.util.find_library(libname_base) + lib = ctypes.util.find_library(libname_base) else: - return ctypes.util.find_library(libname) + lib = ctypes.util.find_library(libname) + if lib and os.path.sep not in lib: + # work around https://github.com/python/cpython/issues/65241, + # where python does not return the absolute path on *nix + try: + libname = lib + ' ' + with subprocess.Popen(['/sbin/ldconfig', '-p'], + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + env={'LC_ALL': 'C', 'LANG': 'C'}) as p: + for line in os.fsdecode(p.stdout.read()).splitlines(): + if line.lstrip().startswith(libname): + return os.path.realpath(line.split()[-1]) + except: + pass + return lib def find_executable(exename, cwd=True, include_PATH=True, pathlist=None): From ebb4d0768f30dbfaf09d56db55f7973ce6bb554a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:38:21 -0600 Subject: [PATCH 0974/1178] bugfix: add missing import --- pyomo/contrib/simplification/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 2c7b1830ff6..79bc9970241 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -17,6 +17,7 @@ from distutils.dist import Distribution from pybind11.setup_helpers import Pybind11Extension, build_ext +from pyomo.common.cmake_builder import handleReadonly from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import find_library, this_file_dir From 16d49e13605cf1c8c4a3ba1a4079b5d751d55791 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:38:54 -0600 Subject: [PATCH 0975/1178] NFC: clarify ginac builder exception message --- pyomo/contrib/simplification/build.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 79bc9970241..508acb2d5a1 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -34,7 +34,9 @@ def build_ginac_interface(args=None): ginac_lib = find_library('ginac') if ginac_lib is None: raise RuntimeError( - 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' + 'could not find the GiNaC library; please make sure either to install ' + 'the library and development headers system-wide, or include the ' + 'path tt the library in the LD_LIBRARY_PATH environment variable' ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) @@ -45,7 +47,9 @@ def build_ginac_interface(args=None): cln_lib = find_library('cln') if cln_lib is None: raise RuntimeError( - 'could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable' + 'could not find the CLN library; please make sure either to install ' + 'the library and development headers system-wide, or include the ' + 'path tt the library in the LD_LIBRARY_PATH environment variable' ) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) From bd3299d4e50ac307947a055a2723ea85bdc6c534 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:40:15 -0600 Subject: [PATCH 0976/1178] Register the GiNaC interface builder with the ExtensionBuilder --- pyomo/contrib/simplification/build.py | 8 ++++++++ pyomo/contrib/simplification/plugins.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 pyomo/contrib/simplification/plugins.py diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 508acb2d5a1..4952ac6dade 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -109,5 +109,13 @@ def run(self): dist.run_command('build_ext') +class GiNaCInterfaceBuilder(object): + def __call__(self, parallel): + return build_ginac_interface() + + def skip(self): + return not find_library('ginac') + + if __name__ == '__main__': build_ginac_interface(sys.argv[1:]) diff --git a/pyomo/contrib/simplification/plugins.py b/pyomo/contrib/simplification/plugins.py new file mode 100644 index 00000000000..6b08f7be4d7 --- /dev/null +++ b/pyomo/contrib/simplification/plugins.py @@ -0,0 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.extensions import ExtensionBuilderFactory +from .build import GiNaCInterfaceBuilder + + +def load(): + ExtensionBuilderFactory.register('ginac')(GiNaCInterfaceBuilder) From 29b207246e4ca83f26c96fec1989b6cea24b79e9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:41:22 -0600 Subject: [PATCH 0977/1178] Rework GiNaC interface builder (in development - testing several things) - attempt to install from OS package repo - attempt local build ginac, add the built library to the download cache --- .github/workflows/test_branches.yml | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3bfb902b9e0..a23a603430c 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -157,24 +157,6 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install GiNaC - if: matrix.other == '/singletest' - run: | - cd .. - curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 - tar -xvf cln-1.3.7.tar.bz2 - cd cln-1.3.7 - ./configure - make -j 2 - sudo make install - cd .. - curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 - tar -xvf ginac-1.8.7.tar.bz2 - cd ginac-1.8.7 - ./configure - make -j 2 - sudo make install - - name: TPL package download cache uses: actions/cache@v4 if: ${{ ! matrix.slim }} @@ -206,7 +188,7 @@ jobs: # Notes: # - install glpk # - pyodbc needs: gcc pkg-config unixodbc freetds - for pkg in bash pkg-config unixodbc freetds glpk; do + for pkg in bash pkg-config unixodbc freetds glpk ginac; do brew list $pkg || brew install $pkg done @@ -218,7 +200,7 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils + install libopenblas-dev gfortran liblapack-dev glpk-utils libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -581,6 +563,32 @@ jobs: echo "$GJH_DIR" ls -l $GJH_DIR + - name: Install GiNaC + if: ${{ ! matrix.slim }} + run: | + if test ! -e "${DOWNLOAD_DIR}/ginac.tar.gz"; then + mkdir -p "${GITHUB_WORKSPACE}/cache/build/ginac" + cd "${GITHUB_WORKSPACE}/cache/build/ginac" + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 + ./configure --prefix "$TPL_DIR/ginac" --disable-static + make -j 4 + make install + cd "${GITHUB_WORKSPACE}/cache/build/ginac" + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure --prefix "$TPL_DIR/ginac" --disable-static + make -j 4 + make install + cd "$TPL_DIR" + tar -czf "${DOWNLOAD_DIR}/ginac.tar.gz" ginac + else + cd "$TPL_DIR" + tar -xzf "${DOWNLOAD_DIR}/ginac.tar.gz" + fi + - name: Install Pyomo run: | echo "" @@ -635,14 +643,6 @@ jobs: echo "" pyomo build-extensions --parallel 2 - - name: Install GiNaC Interface - if: matrix.other == '/singletest' - run: | - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace - - name: Report pyomo plugin information run: | echo "$PATH" From 1b1f944dbd00ca0e0b27d2fd2b70ee681b8e44ae Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 29 Apr 2024 16:58:04 -0600 Subject: [PATCH 0978/1178] bugfix --- pyomo/contrib/simplification/simplify.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 5e251ca326a..27da5f5ca34 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -11,7 +11,7 @@ from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression -from pyomo.core.expr.numvalue import is_fixed, value +from pyomo.core.expr.numvalue import value, is_constant import logging import warnings @@ -28,15 +28,19 @@ def simplify_with_sympy(expr: NumericExpression): + if is_constant(expr): + return value(expr) om, se = sympyify_expression(expr) se = se.simplify() new_expr = sympy2pyomo_expression(se, om) - if is_fixed(new_expr): + if is_constant(new_expr): new_expr = value(new_expr) return new_expr def simplify_with_ginac(expr: NumericExpression, ginac_interface): + if is_constant(expr): + return value(expr) gi = ginac_interface ginac_expr = gi.to_ginac(expr) ginac_expr = ginac_expr.normal() From 74052eebd65a15cf03f36ade73846ec9fcf048e5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 21:58:32 -0600 Subject: [PATCH 0979/1178] Disable local build of GiNaC --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5410c58495f..d1b6b96807b 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -565,7 +565,7 @@ jobs: ls -l $GJH_DIR - name: Install GiNaC - if: ${{ ! matrix.slim }} + if: ${{ 0 && ! matrix.slim }} run: | if test ! -e "${DOWNLOAD_DIR}/ginac.tar.gz"; then mkdir -p "${GITHUB_WORKSPACE}/cache/build/ginac" From 6bb0f7f375e7a10d4daa05841c4dd5544eff99ff Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 21:59:34 -0600 Subject: [PATCH 0980/1178] NFC: apply black --- pyomo/common/fileutils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 80fd47a6377..7b6520327a0 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -384,11 +384,13 @@ def find_library(libname, cwd=True, include_PATH=True, pathlist=None): # where python does not return the absolute path on *nix try: libname = lib + ' ' - with subprocess.Popen(['/sbin/ldconfig', '-p'], - stdin=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - stdout=subprocess.PIPE, - env={'LC_ALL': 'C', 'LANG': 'C'}) as p: + with subprocess.Popen( + ['/sbin/ldconfig', '-p'], + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + env={'LC_ALL': 'C', 'LANG': 'C'}, + ) as p: for line in os.fsdecode(p.stdout.read()).splitlines(): if line.lstrip().startswith(libname): return os.path.realpath(line.split()[-1]) From 39643b336ef3149fe7f57007ffaf31b684b6151d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:05:00 -0600 Subject: [PATCH 0981/1178] remove repeated code --- pyomo/contrib/appsi/build.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index b3d78467f01..38f8cb713ca 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -16,15 +16,6 @@ import tempfile -def handleReadonly(function, path, excinfo): - excvalue = excinfo[1] - if excvalue.errno == errno.EACCES: - os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 - function(path) - else: - raise - - def get_appsi_extension(in_setup=False, appsi_root=None): from pybind11.setup_helpers import Pybind11Extension @@ -66,6 +57,7 @@ def build_appsi(args=[]): from setuptools import Distribution from pybind11.setup_helpers import build_ext import pybind11.setup_helpers + from pyomo.common.cmake_builder import handleReadonly from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import this_file_dir From 89ace9bed0c1a0366ac14dca0d627df0a89b1f01 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:18:31 -0600 Subject: [PATCH 0982/1178] Add support for download tar archives to theFileDownloader --- pyomo/common/download.py | 48 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 5332287cfc7..95713e9ef76 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -29,6 +29,7 @@ urllib_error = attempt_import('urllib.error')[0] ssl = attempt_import('ssl')[0] zipfile = attempt_import('zipfile')[0] +tarfile = attempt_import('tarfile')[0] gzip = attempt_import('gzip')[0] distro, distro_available = attempt_import('distro') @@ -371,7 +372,7 @@ def get_zip_archive(self, url, dirOffset=0): # Simple sanity checks for info in zip_file.infolist(): f = info.filename - if f[0] in '\\/' or '..' in f: + if f[0] in '\\/' or '..' in f or os.path.isabs(f): logger.error( "malformed (potentially insecure) filename (%s) " "found in zip archive. Skipping file." % (f,) @@ -387,6 +388,51 @@ def get_zip_archive(self, url, dirOffset=0): info.filename = target[-1] + '/' if f[-1] == '/' else target[-1] zip_file.extract(f, os.path.join(self._fname, *tuple(target[dirOffset:-1]))) + def get_tar_archive(self, url, dirOffset=0): + if self._fname is None: + raise DeveloperError( + "target file name has not been initialized " + "with set_destination_filename" + ) + if os.path.exists(self._fname) and not os.path.isdir(self._fname): + raise RuntimeError( + "Target directory (%s) exists, but is not a directory" % (self._fname,) + ) + tar_file = tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) + dest = os.path.realpath(self._fname) + + def filter_fcn(info): + # this mocks up the `tarfile` filter introduced in Python + # 3.12 and backported to later releases of Python (e.g., + # 3.8.17, 3.9.17, 3.10.12, and 3.11.4) + f = info.name + if os.path.isabs(f) or '..' in f or f.startswith(('/', os.sep)): + logger.error( + "malformed (potentially insecure) filename (%s) " + "found in tar archive. Skipping file." % (f,) + ) + return False + target = os.path.realpath(os.path.join(dest, f)) + if os.path.commonpath([target, dest]) != dest: + logger.error( + "malformed (potentially insecure) filename (%s) " + "found in zip archive. Skipping file." % (f,) + ) + return False + target = self._splitpath(f) + if len(target) <= dirOffset: + if not info.isdir(): + logger.warning( + "Skipping file (%s) in zip archive due to dirOffset" % (f,) + ) + return False + info.name = '/'.join(target[dirOffset:]) + # Strip high bits & group/other write bits + info.mode &= 0o755 + return True + + tar_file.extractall(dest, filter(filter_fcn, tar_file.getmembers())) + def get_gzipped_binary_file(self, url): if self._fname is None: raise DeveloperError( From bb274ff9d30960f72ffbbf001850d64a14caa6f6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:20:08 -0600 Subject: [PATCH 0983/1178] Switch GiNaC interface builder to use TempfileManager --- pyomo/contrib/simplification/build.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 4952ac6dade..d30f582bcea 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -71,13 +71,13 @@ def build_ginac_interface(args=None): class ginacBuildExt(build_ext): def run(self): basedir = os.path.abspath(os.path.curdir) - if self.inplace: - tmpdir = this_file_dir() - else: - tmpdir = os.path.abspath(tempfile.mkdtemp()) - print("Building in '%s'" % tmpdir) - os.chdir(tmpdir) - try: + with TempfileManager.new_context() as tempfile: + if self.inplace: + tmpdir = this_file_dir() + else: + tmpdir = os.path.abspath(tempfile.mkdtemp()) + print("Building in '%s'" % tmpdir) + os.chdir(tmpdir) super(ginacBuildExt, self).run() if not self.inplace: library = glob.glob("build/*/ginac_interface.*")[0] @@ -91,10 +91,6 @@ def run(self): if not os.path.exists(target): os.makedirs(target) shutil.copy(library, target) - finally: - os.chdir(basedir) - if not self.inplace: - shutil.rmtree(tmpdir, onerror=handleReadonly) package_config = { 'name': 'ginac_interface', From adaefbca72f94548b33328b7a28c2e03a4012a9d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:20:39 -0600 Subject: [PATCH 0984/1178] Add function for downloading and installing GiNaC and CLN --- pyomo/contrib/simplification/build.py | 83 ++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d30f582bcea..3ea7748cdbf 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -10,19 +10,71 @@ # ___________________________________________________________________________ import glob +import logging import os import shutil import sys -import tempfile -from distutils.dist import Distribution +import subprocess -from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.cmake_builder import handleReadonly +from pyomo.common.download import FileDownloader from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import find_library, this_file_dir +from pyomo.common.tempfiles import TempfileManager -def build_ginac_interface(args=None): +logger = logging.getLogger(__name__) + + +def build_ginac_library(parallel=None, argv=None): + print("\n**** Building GiNaC library ****") + + configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] + make_cmd = ['make'] + if parallel: + make_cmd.append(f'-j{parallel}') + install_cmd = ['make', 'install'] + + with TempfileManager.new_context() as tempfile: + tmpdir = tempfile.mkdtemp() + + downloader = FileDownloader() + if argv: + downloader.parse_args(argv) + + url = 'https://www.ginac.de/CLN/cln-1.3.7.tar.bz2' + cln_dir = os.path.join(tmpdir, 'cln') + downloader.set_destination_filename(cln_dir) + logger.info( + "Fetching CLN from %s and installing it to %s" + % (url, downloader.destination()) + ) + downloader.get_tar_archive(url, dirOffset=1) + assert subprocess.run(configure_cmd, cwd=cln_dir).returncode == 0 + logger.info("\nBuilding CLN\n") + assert subprocess.run(make_cmd, cwd=cln_dir).returncode == 0 + assert subprocess.run(install_cmd, cwd=cln_dir).returncode == 0 + + url = 'https://www.ginac.de/ginac-1.8.7.tar.bz2' + ginac_dir = os.path.join(tmpdir, 'ginac') + downloader.set_destination_filename(ginac_dir) + logger.info( + "Fetching GiNaC from %s and installing it to %s" + % (url, downloader.destination()) + ) + downloader.get_tar_archive(url, dirOffset=1) + assert subprocess.run(configure_cmd, cwd=ginac_dir).returncode == 0 + logger.info("\nBuilding GiNaC\n") + assert subprocess.run(make_cmd, cwd=ginac_dir).returncode == 0 + assert subprocess.run(install_cmd, cwd=ginac_dir).returncode == 0 + + +def build_ginac_interface(parallel=None, args=None): + from distutils.dist import Distribution + from pybind11.setup_helpers import Pybind11Extension, build_ext + from pyomo.common.cmake_builder import handleReadonly + + print("\n**** Building GiNaC interface ****") + if args is None: args = list() dname = this_file_dir() @@ -107,11 +159,28 @@ def run(self): class GiNaCInterfaceBuilder(object): def __call__(self, parallel): - return build_ginac_interface() + return build_ginac_interface(parallel) def skip(self): return not find_library('ginac') if __name__ == '__main__': - build_ginac_interface(sys.argv[1:]) + logging.getLogger('pyomo').setLevel(logging.DEBUG) + parallel = None + for i, arg in enumerate(sys.argv): + if arg == '-j': + parallel = int(sys.argv.pop(i + 1)) + sys.argv.pop(i) + break + if arg.startswith('-j'): + if '=' in arg: + parallel = int(arg.split('=')[1]) + else: + parallel = int(arg[2:]) + sys.argv.pop(i) + break + if '--build-deps' in sys.argv: + sys.argv.remove('--build-deps') + build_ginac_library(parallel, []) + build_ginac_interface(parallel, sys.argv[1:]) From c7a8f8e243c2a51bf5e74803b1fc118fd4060816 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:21:10 -0600 Subject: [PATCH 0985/1178] Hook GiNaC builder into pyomo command --- pyomo/environ/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index c1ceb8cb890..07b3dfad680 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -50,6 +50,7 @@ def _do_import(pkg_name): 'pyomo.contrib.multistart', 'pyomo.contrib.preprocessing', 'pyomo.contrib.pynumero', + 'pyomo.contrib.simplification', 'pyomo.contrib.solver', 'pyomo.contrib.trustregion', ] From a29bb3ed8149dc3b1221ce858cce5640716e5c14 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:23:53 -0600 Subject: [PATCH 0986/1178] Remove simplification test marker --- .github/workflows/test_branches.yml | 5 ----- pyomo/contrib/simplification/tests/test_simplification.py | 1 - setup.cfg | 1 - 3 files changed, 7 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d1b6b96807b..558d6dc2591 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -651,11 +651,6 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo tests if: matrix.mpi == 0 run: | diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 1a5ae1e0036..be61631e9f3 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -106,7 +106,6 @@ class TestSimplificationSympy(TestCase, SimplificationMixin): @unittest.skipIf(not ginac_available, 'GiNaC is not available') -@unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() diff --git a/setup.cfg b/setup.cfg index 855717490b3..b606138f38c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,3 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - simplification: tests for expression simplification that have expensive (to install) dependencies From c475fe791ff6c60aa75ae8eb87a5cabb8d8786ab Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:33:38 -0600 Subject: [PATCH 0987/1178] Switching output to sys.stdout, adding debugging --- pyomo/contrib/simplification/build.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 3ea7748cdbf..5e613cf873f 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -26,7 +26,7 @@ def build_ginac_library(parallel=None, argv=None): - print("\n**** Building GiNaC library ****") + sys.stdout.write("\n**** Building GiNaC library ****") configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] make_cmd = ['make'] @@ -73,7 +73,7 @@ def build_ginac_interface(parallel=None, args=None): from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.cmake_builder import handleReadonly - print("\n**** Building GiNaC interface ****") + sys.stdout.write("\n**** Building GiNaC interface ****") if args is None: args = list() @@ -90,6 +90,7 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) + print("Found GiNaC library:", ginac_lib) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) ginac_include_dir = os.path.join(ginac_build_dir, 'include') @@ -103,6 +104,7 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) + print("Found CLN library:", cln_lib) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) cln_include_dir = os.path.join(cln_build_dir, 'include') @@ -128,7 +130,7 @@ def run(self): tmpdir = this_file_dir() else: tmpdir = os.path.abspath(tempfile.mkdtemp()) - print("Building in '%s'" % tmpdir) + sys.stdout.write("Building in '%s'" % tmpdir) os.chdir(tmpdir) super(ginacBuildExt, self).run() if not self.inplace: From 7053690e90b0a693ec8901d9b4c73279a85a41aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:45:14 -0600 Subject: [PATCH 0988/1178] Support walking up the directory tree looking for ginac headers (this should better support debian system installations) --- pyomo/contrib/simplification/build.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 5e613cf873f..e6f300ae058 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -68,6 +68,16 @@ def build_ginac_library(parallel=None, argv=None): assert subprocess.run(install_cmd, cwd=ginac_dir).returncode == 0 +def _find_include(libdir, incpaths): + while 1: + basedir = os.path.dirname(libdir) + if not basedir or basedir == libdir: + return None + if os.path.exists(os.path.join(basedir, *incpaths)): + return os.path.join(basedir, *(incpaths[:-1]))): + libdir = basedir + + def build_ginac_interface(parallel=None, args=None): from distutils.dist import Distribution from pybind11.setup_helpers import Pybind11Extension, build_ext @@ -90,11 +100,9 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) - print("Found GiNaC library:", ginac_lib) ginac_lib_dir = os.path.dirname(ginac_lib) - ginac_build_dir = os.path.dirname(ginac_lib_dir) - ginac_include_dir = os.path.join(ginac_build_dir, 'include') - if not os.path.exists(os.path.join(ginac_include_dir, 'ginac', 'ginac.h')): + ginac_include_dir = _find_include(ginac_lib_dir, ('ginac', 'ginac.h')) + if not ginac_include_dir: raise RuntimeError('could not find GiNaC include directory') cln_lib = find_library('cln') @@ -104,11 +112,9 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) - print("Found CLN library:", cln_lib) cln_lib_dir = os.path.dirname(cln_lib) - cln_build_dir = os.path.dirname(cln_lib_dir) - cln_include_dir = os.path.join(cln_build_dir, 'include') - if not os.path.exists(os.path.join(cln_include_dir, 'cln', 'cln.h')): + cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) + if cln_include_dir: raise RuntimeError('could not find CLN include directory') extra_args = ['-std=c++11'] From 7126fb7a0258524d4a1233fd16a5931ef064fe08 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 10:57:09 +0200 Subject: [PATCH 0989/1178] Modified two tests --- .../appsi/solvers/tests/test_persistent_solvers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index d6df1710a03..d38563844ff 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1026,7 +1026,7 @@ def test_time_limit( self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest from sys import platform @@ -1210,20 +1210,23 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) + + if type(opt) is MAiNGO: + opt.config.mip_gap = 1e-6 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From 673fd372e55c54496cd01446e248ba9ab3d96024 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 12:33:04 +0200 Subject: [PATCH 0990/1178] Modify test_persistent_solvers.py --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index d38563844ff..3db2ae3cba1 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1211,7 +1211,7 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if type(opt) is MAiNGO: + if opt_class == MAiNGO: opt.config.mip_gap = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0) From 0127d29aeadf2034fde7b95b962d89c6c58488a6 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 13:21:54 +0200 Subject: [PATCH 0991/1178] Set default mipgap higher --- pyomo/contrib/appsi/solvers/maingo.py | 4 ++-- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 944673be53d..e52130061f7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -110,7 +110,7 @@ def __init__( 'epsilonA', ConfigValue( domain=NonNegativeFloat, - default=1e-4, + default=1e-5, description="Absolute optimality tolerance", ), ) @@ -118,7 +118,7 @@ def __init__( 'epsilonR', ConfigValue( domain=NonNegativeFloat, - default=1e-4, + default=1e-5, description="Relative optimality tolerance", ), ) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 3db2ae3cba1..6ab36ccc981 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1211,8 +1211,6 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if opt_class == MAiNGO: - opt.config.mip_gap = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) From 389740c54cdae96ef27375fc2b03e59c9002d036 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 13:42:54 +0200 Subject: [PATCH 0992/1178] Modify test_fixed_binaries --- .../appsi/solvers/tests/test_persistent_solvers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 6ab36ccc981..7ff193b38e4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1212,19 +1212,19 @@ def test_fixed_binaries( m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From be6922453be5ef7da898235b122d251d1222ad04 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 07:31:44 -0600 Subject: [PATCH 0993/1178] Fix several typos / include search logic --- pyomo/contrib/simplification/build.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index e6f300ae058..fb571c273eb 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -69,12 +69,13 @@ def build_ginac_library(parallel=None, argv=None): def _find_include(libdir, incpaths): + rel_path = ('include',) + incpaths while 1: basedir = os.path.dirname(libdir) if not basedir or basedir == libdir: return None - if os.path.exists(os.path.join(basedir, *incpaths)): - return os.path.join(basedir, *(incpaths[:-1]))): + if os.path.exists(os.path.join(basedir, *rel_path)): + return os.path.join(basedir, *(rel_path[:-len(incpaths)])) libdir = basedir @@ -86,15 +87,13 @@ def build_ginac_interface(parallel=None, args=None): sys.stdout.write("\n**** Building GiNaC interface ****") if args is None: - args = list() + args = [] dname = this_file_dir() _sources = ['ginac_interface.cpp'] - sources = list() - for fname in _sources: - sources.append(os.path.join(dname, fname)) + sources = [os.path.join(dname, fname) for fname in _sources] ginac_lib = find_library('ginac') - if ginac_lib is None: + if not ginac_lib: raise RuntimeError( 'could not find the GiNaC library; please make sure either to install ' 'the library and development headers system-wide, or include the ' @@ -106,7 +105,7 @@ def build_ginac_interface(parallel=None, args=None): raise RuntimeError('could not find GiNaC include directory') cln_lib = find_library('cln') - if cln_lib is None: + if not cln_lib: raise RuntimeError( 'could not find the CLN library; please make sure either to install ' 'the library and development headers system-wide, or include the ' @@ -114,7 +113,7 @@ def build_ginac_interface(parallel=None, args=None): ) cln_lib_dir = os.path.dirname(cln_lib) cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) - if cln_include_dir: + if not cln_include_dir: raise RuntimeError('could not find CLN include directory') extra_args = ['-std=c++11'] From 547b3015eed3157b3a4023a5e31ca53d1b598e57 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 15:32:58 +0200 Subject: [PATCH 0994/1178] Set epsilonA for one test --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7ff193b38e4..7fa2a62a8be 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1210,7 +1210,8 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - + if type(opt) is MAiNGO: + opt.maingo_options["epsilonA"] = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) From 4ac04a6f3e8c4511f922d00c5eb7430bb8ee9b2a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 07:33:23 -0600 Subject: [PATCH 0995/1178] NFC: apply black --- pyomo/contrib/simplification/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index fb571c273eb..a4094f993fa 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -75,7 +75,7 @@ def _find_include(libdir, incpaths): if not basedir or basedir == libdir: return None if os.path.exists(os.path.join(basedir, *rel_path)): - return os.path.join(basedir, *(rel_path[:-len(incpaths)])) + return os.path.join(basedir, *(rel_path[: -len(incpaths)])) libdir = basedir From abe5f8bb136bd1faadbcc4340deabfa42061d566 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 07:47:07 -0600 Subject: [PATCH 0996/1178] Resync GHA workflows, remove ginac build code --- .github/workflows/test_branches.yml | 31 +++------------------ .github/workflows/test_pr_and_main.yml | 37 +++----------------------- 2 files changed, 7 insertions(+), 61 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 558d6dc2591..de40066b50f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -201,7 +201,8 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils libginac-dev + install libopenblas-dev gfortran liblapack-dev glpk-utils \ + libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -346,7 +347,7 @@ jobs: echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) - conda install --update-deps -y $CONDA_DEPENDENCIES + conda install --update-deps -q -y $CONDA_DEPENDENCIES if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" @@ -564,32 +565,6 @@ jobs: echo "$GJH_DIR" ls -l $GJH_DIR - - name: Install GiNaC - if: ${{ 0 && ! matrix.slim }} - run: | - if test ! -e "${DOWNLOAD_DIR}/ginac.tar.gz"; then - mkdir -p "${GITHUB_WORKSPACE}/cache/build/ginac" - cd "${GITHUB_WORKSPACE}/cache/build/ginac" - curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 - tar -xvf cln-1.3.7.tar.bz2 - cd cln-1.3.7 - ./configure --prefix "$TPL_DIR/ginac" --disable-static - make -j 4 - make install - cd "${GITHUB_WORKSPACE}/cache/build/ginac" - curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 - tar -xvf ginac-1.8.7.tar.bz2 - cd ginac-1.8.7 - ./configure --prefix "$TPL_DIR/ginac" --disable-static - make -j 4 - make install - cd "$TPL_DIR" - tar -czf "${DOWNLOAD_DIR}/ginac.tar.gz" ginac - else - cd "$TPL_DIR" - tar -xzf "${DOWNLOAD_DIR}/ginac.tar.gz" - fi - - name: Install Pyomo run: | echo "" diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 7c84ed14093..cdc42718cba 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -187,24 +187,6 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install GiNaC - if: matrix.other == '/singletest' - run: | - cd .. - curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 - tar -xvf cln-1.3.7.tar.bz2 - cd cln-1.3.7 - ./configure - make -j 2 - sudo make install - cd .. - curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 - tar -xvf ginac-1.8.7.tar.bz2 - cd ginac-1.8.7 - ./configure - make -j 2 - sudo make install - - name: TPL package download cache uses: actions/cache@v4 if: ${{ ! matrix.slim }} @@ -236,7 +218,7 @@ jobs: # Notes: # - install glpk # - pyodbc needs: gcc pkg-config unixodbc freetds - for pkg in bash pkg-config unixodbc freetds glpk; do + for pkg in bash pkg-config unixodbc freetds glpk ginac; do brew list $pkg || brew install $pkg done @@ -248,7 +230,8 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils + install libopenblas-dev gfortran liblapack-dev glpk-utils \ + libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -389,6 +372,7 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + echo "" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) @@ -664,14 +648,6 @@ jobs: echo "" pyomo build-extensions --parallel 2 - - name: Install GiNaC Interface - if: matrix.other == '/singletest' - run: | - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace - - name: Report pyomo plugin information run: | echo "$PATH" @@ -679,11 +655,6 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo tests if: matrix.mpi == 0 run: | From 0c14380b675f55a49f238b675055a1557e191ce5 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 16:02:17 +0200 Subject: [PATCH 0997/1178] Modify fixed_binary-test --- .../appsi/solvers/tests/test_persistent_solvers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7fa2a62a8be..67088297cf4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1210,22 +1210,20 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if type(opt) is MAiNGO: - opt.maingo_options["epsilonA"] = 1e-6 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From 539d62dc2fc9dea7afe4c838dac5018921f67171 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 08:36:34 -0700 Subject: [PATCH 0998/1178] simplified use of suffix update and removed extra prints --- .../simple_reaction_parmest_example.py | 8 +++---- .../reactor_design/bootstrap_example.py | 2 +- .../confidence_region_example.py | 4 ++-- .../reactor_design/datarec_example.py | 24 +++++++++---------- .../reactor_design/leaveNout_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../multisensor_data_example.py | 15 ++++++------ .../parameter_estimation_example.py | 2 +- .../examples/reactor_design/reactor_design.py | 8 +++---- .../reactor_design/timeseries_data_example.py | 2 +- .../rooney_biegler/bootstrap_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../parameter_estimation_example.py | 2 +- .../examples/rooney_biegler/rooney_biegler.py | 4 ++-- .../rooney_biegler_with_constraint.py | 4 ++-- .../semibatch/parameter_estimation_example.py | 2 +- .../examples/semibatch/scenario_example.py | 2 +- pyomo/contrib/parmest/tests/test_parmest.py | 8 +++---- 18 files changed, 47 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index e5bfd99c84f..dcfca900f28 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -89,9 +89,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.x1, self.data['x1'])]) - m.experiment_outputs.update([(m.x2, self.data['x2'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.x1, self.data['x1']), + (m.x2, self.data['x2']), + (m.y, self.data['y'])]) return m @@ -156,7 +156,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # ======================================================================= # Parameter estimation without covariance estimate diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index f845930ab79..598fef32b60 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -31,7 +31,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py index 8aee6e9d67c..73129baf5cb 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -31,7 +31,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index e05b69aa4cc..db03b268178 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -40,17 +40,17 @@ def label_model(self): # experiment outputs m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) - m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) - m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) - m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + m.experiment_outputs.update([(m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd'])]) # experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) - m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) - m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) - m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd'])]) # no unknowns (theta names) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -71,10 +71,10 @@ def label_model(self): # add experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) - m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) - m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) - m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd'])]) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index c735b191e0c..9560981ca5c 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -38,7 +38,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index 45adaa27e7f..c2bff254077 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -32,7 +32,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index 95bcf211207..d0136fa6f92 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -41,12 +41,11 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - [(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])] - ) - m.experiment_outputs.update([(m.cb, [self.data_i['cb']])]) - m.experiment_outputs.update([(m.cc, [self.data_i['cc1'], self.data_i['cc2']])]) - m.experiment_outputs.update([(m.cd, [self.data_i['cd']])]) + m.experiment_outputs.update([ + (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), + (m.cb, [self.data_i['cb']]), + (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), + (m.cd, [self.data_i['cd']])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -80,8 +79,8 @@ def SSE_multisensor(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) - # print(SSE_multisensor(exp0_model)) + # exp0_model.pprint() + # SSE_multisensor(exp0_model) pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index d72b7aa9878..a84a3fde5e7 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -31,7 +31,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index a2025b8a324..7918d8a14cd 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -107,10 +107,10 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) - m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) - m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) - m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + m.experiment_outputs.update([(m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 1e457bf1e89..04a64850f40 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -68,7 +68,7 @@ def SSE_timeseries(model): # View one model & SSE # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # print(SSE_timeseries(exp0_model)) pest = parmest.Estimator(exp_list, obj_function=SSE_timeseries) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 04917e1a817..944a01ac95e 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -39,7 +39,7 @@ def SSE(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index d8b572890ba..54343993286 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -40,7 +40,7 @@ def SSE(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 18c4904787b..3c9a93100bb 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -39,7 +39,7 @@ def SSE(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index a9918cf2268..7b4dc289061 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -61,8 +61,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour']), + (m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 259fa45785a..4a2a07a052d 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -65,8 +65,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour']), + (m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index 0d5416c714a..7eafdd2b9c3 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -33,7 +33,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Note, the model already includes a 'SecondStageCost' expression # for sum of squared error that will be used in parameter estimation diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index b2e9f7bd7ee..697cb9ac7a5 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -34,7 +34,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 9c65a31352f..e9cbfcebb7f 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -427,8 +427,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"])]) - m.experiment_outputs.update([(m.y, self.data["y"])]) + m.experiment_outputs.update([(m.hour, self.data["hour"]), + (m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -506,8 +506,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"])]) - m.experiment_outputs.update([(m.y, self.data["y"])]) + m.experiment_outputs.update([(m.hour, self.data["hour"]), + (m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) From b7dc0f1ac535f46ba7e1051211e6250bbb2824d6 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 08:50:23 -0700 Subject: [PATCH 0999/1178] minor update, added print --- .../parmest/examples/reactor_design/multisensor_data_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index d0136fa6f92..f2820edf6a6 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -80,7 +80,7 @@ def SSE_multisensor(model): # View one model # exp0_model = exp_list[0].get_labeled_model() # exp0_model.pprint() - # SSE_multisensor(exp0_model) + # print(SSE_multisensor(exp0_model)) pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() From 032fd3102ec6e933eb51350ed1467d106cc0d986 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 10:07:39 -0600 Subject: [PATCH 1000/1178] Update deprecation version in scenariocreator.py --- pyomo/contrib/parmest/scenariocreator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index f2798ad2e94..2208bde91a0 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -17,7 +17,7 @@ from pyomo.common.deprecation import deprecated from pyomo.common.deprecation import deprecation_warning -DEPRECATION_VERSION = '6.7.0' +DEPRECATION_VERSION = '6.7.2.dev0' import logging From 9c5f8df5266a7adaa271ab1904c1a0c6e3c1dd36 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:12:39 -0700 Subject: [PATCH 1001/1178] reformatted lists --- .../simple_reaction_parmest_example.py | 6 ++-- .../reactor_design/datarec_example.py | 36 ++++++++++++------- .../multisensor_data_example.py | 13 ++++--- .../examples/reactor_design/reactor_design.py | 12 ++++--- .../examples/rooney_biegler/rooney_biegler.py | 5 +-- .../rooney_biegler_with_constraint.py | 5 +-- pyomo/contrib/parmest/tests/test_parmest.py | 10 +++--- 7 files changed, 55 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index dcfca900f28..5c8a0219946 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -89,9 +89,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.x1, self.data['x1']), - (m.x2, self.data['x2']), - (m.y, self.data['y'])]) + m.experiment_outputs.update( + [(m.x1, self.data['x1']), (m.x2, self.data['x2']), (m.y, self.data['y'])] + ) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index db03b268178..ba41bbfb7b8 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -40,17 +40,25 @@ def label_model(self): # experiment outputs m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca']), - (m.cb, self.data_i['cb']), - (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd'])]) + m.experiment_outputs.update( + [ + (m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd']) + ] + ) # experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), - (m.cb, self.data_std['cb']), - (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update( + [ + (m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd']) + ] + ) # no unknowns (theta names) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -71,10 +79,14 @@ def label_model(self): # add experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), - (m.cb, self.data_std['cb']), - (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update( + [ + (m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd']) + ] + ) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index f2820edf6a6..e7e4fb1b04d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -41,11 +41,14 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([ - (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), - (m.cb, [self.data_i['cb']]), - (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), - (m.cd, [self.data_i['cd']])]) + m.experiment_outputs.update( + [ + (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), + (m.cb, [self.data_i['cb']]), + (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), + (m.cd, [self.data_i['cd']]) + ] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 7918d8a14cd..7f6e46cc723 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -107,10 +107,14 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca']), - (m.cb, self.data_i['cb']), - (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd'])]) + m.experiment_outputs.update( + [ + (m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd']) + ] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 7b4dc289061..9625ab32ea3 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -61,8 +61,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour']), - (m.y, self.data['y'])]) + m.experiment_outputs.update( + [(m.hour, self.data['hour']), (m.y, self.data['y'])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 4a2a07a052d..dd82b50cf7a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -65,8 +65,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour']), - (m.y, self.data['y'])]) + m.experiment_outputs.update( + [(m.hour, self.data['hour']), (m.y, self.data['y'])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index e9cbfcebb7f..e9a8e089335 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -427,8 +427,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"]), - (m.y, self.data["y"])]) + m.experiment_outputs.update( + [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -506,8 +507,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"]), - (m.y, self.data["y"])]) + m.experiment_outputs.update( + [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) From bf865f4383e93402548db9cd548ebbace383bb80 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 10:14:20 -0600 Subject: [PATCH 1002/1178] Update deprecation version in parmest.py --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index aecc9d5ebc2..a1200e2c3a5 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -78,7 +78,7 @@ from pyomo.common.deprecation import deprecated from pyomo.common.deprecation import deprecation_warning -DEPRECATION_VERSION = '6.7.0' +DEPRECATION_VERSION = '6.7.2.dev0' parmest_available = numpy_available & pandas_available & scipy_available From db48a25a987ad0f978e6ef3ebc873eabf33f1a92 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:29:22 -0700 Subject: [PATCH 1003/1178] added missing commas --- .../parmest/examples/reactor_design/datarec_example.py | 6 +++--- .../examples/reactor_design/multisensor_data_example.py | 2 +- .../parmest/examples/reactor_design/reactor_design.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index ba41bbfb7b8..02aa13fceab 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -45,7 +45,7 @@ def label_model(self): (m.ca, self.data_i['ca']), (m.cb, self.data_i['cb']), (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd']) + (m.cd, self.data_i['cd']), ] ) @@ -56,7 +56,7 @@ def label_model(self): (m.ca, self.data_std['ca']), (m.cb, self.data_std['cb']), (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd']) + (m.cd, self.data_std['cd']), ] ) @@ -84,7 +84,7 @@ def label_model(self): (m.ca, self.data_std['ca']), (m.cb, self.data_std['cb']), (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd']) + (m.cd, self.data_std['cd']), ] ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index e7e4fb1b04d..48a7bca52ca 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -46,7 +46,7 @@ def label_model(self): (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), (m.cb, [self.data_i['cb']]), (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), - (m.cd, [self.data_i['cd']]) + (m.cd, [self.data_i['cd']]), ] ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 7f6e46cc723..a396c1ea721 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -112,7 +112,7 @@ def label_model(self): (m.ca, self.data_i['ca']), (m.cb, self.data_i['cb']), (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd']) + (m.cd, self.data_i['cd']), ] ) From e68c415176439e9299769e59ac4c25bdbf5f0fad Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:31:22 -0700 Subject: [PATCH 1004/1178] removed _treemaker, not used --- pyomo/contrib/parmest/parmest.py | 37 -------------------------------- 1 file changed, 37 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a1200e2c3a5..c9826a57b1d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -16,7 +16,6 @@ # TODO: move use_mpisppy to a Pyomo configuration option # Redesign TODOS -# TODO: _treemaker is not used in parmest, the code could be moved to scenario tree if needed # TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) # TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) # TODO: Clean up the use of updated_theta_names, model_theta_names, estimator_theta_names. Not sure if estimator_theta_names is the union or intersect of thetas in each model @@ -239,42 +238,6 @@ def _experiment_instance_creation_callback( return instance -# # ============================================= -# def _treemaker(scenlist): -# """ -# Makes a scenario tree (avoids dependence on daps) - -# Parameters -# ---------- -# scenlist (list of `int`): experiment (i.e. scenario) numbers - -# Returns -# ------- -# a `ConcreteModel` that is the scenario tree -# """ - -# num_scenarios = len(scenlist) -# m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() -# m = m.create_instance() -# m.Stages.add('Stage1') -# m.Stages.add('Stage2') -# m.Nodes.add('RootNode') -# for i in scenlist: -# m.Nodes.add('LeafNode_Experiment' + str(i)) -# m.Scenarios.add('Experiment' + str(i)) -# m.NodeStage['RootNode'] = 'Stage1' -# m.ConditionalProbability['RootNode'] = 1.0 -# for node in m.Nodes: -# if node != 'RootNode': -# m.NodeStage[node] = 'Stage2' -# m.Children['RootNode'].add(node) -# m.Children[node].clear() -# m.ConditionalProbability[node] = 1.0 / num_scenarios -# m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - -# return m - - def SSE(model): """ Sum of squared error between `experiment_output` model and data values From 63634c99a913944864aa1450399a05e6c900ab53 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:40:05 -0700 Subject: [PATCH 1005/1178] removed _SecondStageCostExpr class, call objective function directly --- pyomo/contrib/parmest/parmest.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c9826a57b1d..ac2c7fdb0aa 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -246,18 +246,6 @@ def SSE(model): return expr -class _SecondStageCostExpr(object): - """ - Class to pass objective expression into the Pyomo model - """ - - def __init__(self, ssc_function): - self._ssc_function = ssc_function - - def __call__(self, model): - return self._ssc_function(model) - - class Estimator(object): """ Parameter estimation class @@ -419,10 +407,10 @@ def _create_parmest_model(self, experiment_number): # TODO, this needs to be turned a enum class of options that still support custom functions if self.obj_function == 'SSE': - second_stage_rule = _SecondStageCostExpr(SSE) + second_stage_rule = SSE else: # A custom function uses model.experiment_outputs as data - second_stage_rule = _SecondStageCostExpr(self.obj_function) + second_stage_rule = self.obj_function model.FirstStageCost = pyo.Expression(expr=0) model.SecondStageCost = pyo.Expression(rule=second_stage_rule) From a92b4f58ed95a148aa2a9750018c81761f643794 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:53:35 -0700 Subject: [PATCH 1006/1178] changed yhat to y_hat --- .../parmest/examples/reactor_design/datarec_example.py | 4 ++-- .../examples/reactor_design/multisensor_data_example.py | 6 +++--- .../examples/reactor_design/timeseries_data_example.py | 6 +++--- pyomo/contrib/parmest/parmest.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 02aa13fceab..be08e727be9 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -132,8 +132,8 @@ def main(): # Define sum of squared error objective function for data rec def SSE_with_std(model): expr = sum( - ((y - yhat) / model.experiment_outputs_std[y]) ** 2 - for y, yhat in model.experiment_outputs.items() + ((y - y_hat) / model.experiment_outputs_std[y]) ** 2 + for y, y_hat in model.experiment_outputs.items() ) return expr diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index 48a7bca52ca..208981a784a 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -74,10 +74,10 @@ def main(): # Define sum of squared error def SSE_multisensor(model): expr = 0 - for y, yhat in model.experiment_outputs.items(): - num_outputs = len(yhat) + for y, y_hat in model.experiment_outputs.items(): + num_outputs = len(y_hat) for i in range(num_outputs): - expr += ((y - yhat[i]) ** 2) * (1 / num_outputs) + expr += ((y - y_hat[i]) ** 2) * (1 / num_outputs) return expr # View one model diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 04a64850f40..4eb191afd6d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -59,10 +59,10 @@ def main(): def SSE_timeseries(model): expr = 0 - for y, yhat in model.experiment_outputs.items(): - num_time_points = len(yhat) + for y, y_hat in model.experiment_outputs.items(): + num_time_points = len(y_hat) for i in range(num_time_points): - expr += ((y - yhat[i]) ** 2) * (1 / num_time_points) + expr += ((y - y_hat[i]) ** 2) * (1 / num_time_points) return expr diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ac2c7fdb0aa..6bc69c78bcd 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -242,7 +242,7 @@ def SSE(model): """ Sum of squared error between `experiment_output` model and data values """ - expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) + expr = sum((y - y_hat) ** 2 for y, y_hat in model.experiment_outputs.items()) return expr From 097849dbaaadbc1873b8b1dc3a83e24103a55aaf Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 11:45:11 -0600 Subject: [PATCH 1007/1178] Update Estimator constructor in parmest to more robustly support both the new and deprecated APIs --- pyomo/contrib/parmest/parmest.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 6bc69c78bcd..ae6a6ffe184 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -254,7 +254,7 @@ class Estimator(object): ---------- experiment_list: list of Experiments A list of experiment objects which creates one labeled model for - each expeirment + each experiment obj_function: string or function (optional) Built in objective (currently only "SSE") or custom function used to formulate parameter estimation objective. @@ -271,26 +271,25 @@ class Estimator(object): # backwards compatible constructor will accept the old deprecated inputs # as well as the new inputs using experiment lists - def __init__(self, *args, **kwargs): - - # check that we have at least one argument - assert len(args) > 0 + # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor + def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None): # use deprecated interface self.pest_deprecated = None - if callable(args[0]): + if callable(experiment_list): deprecation_warning( - 'Using deprecated parmest inputs (model_function, ' - + 'data, theta_names), please use experiment lists instead.', + 'Using deprecated parmest interface (model_function, ' + 'data, theta_names). This interface will be removed in a future release, ' + 'please update to the new parmest interface using experiment lists.', version=DEPRECATION_VERSION, ) - self.pest_deprecated = _DeprecatedEstimator(*args, **kwargs) + self.pest_deprecated = _DeprecatedEstimator(experiment_list, *args, obj_function, tee, diagnostic_mode, solver_options) return # check that we have a (non-empty) list of experiments - assert isinstance(args[0], list) - assert len(args[0]) > 0 - self.exp_list = args[0] + assert isinstance(experiment_list, list) + assert len(args) == 0 + self.exp_list = experiment_list # check that an experiment has experiment_outputs and unknown_parameters model = self.exp_list[0].get_labeled_model() @@ -308,10 +307,10 @@ def __init__(self, *args, **kwargs): ) # populate keyword argument options - self.obj_function = kwargs.get('obj_function', None) - self.tee = kwargs.get('tee', False) - self.diagnostic_mode = kwargs.get('diagnostic_mode', False) - self.solver_options = kwargs.get('solver_options', None) + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built From 2a9c5337a52ebefadfc72e392d6b72299503d006 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Apr 2024 13:59:21 -0600 Subject: [PATCH 1008/1178] Fixing a typo --- pyomo/contrib/cp/tests/test_docplex_walker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 1173ae66eab..f7abb3d2b3c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1610,7 +1610,7 @@ def test_always_in(self): def test_always_in_single_pulse(self): # This is a bit silly as you can tell whether or not it is feasible - # structurally, but there's not reason it couldn't happen. + # structurally, but there's no reason it couldn't happen. m = self.get_model() f = Pulse((m.i, 3)) m.c = LogicalConstraint(expr=f.within((0, 3), (0, 10))) From efe4dabc5cc14aada83165f7abc3693f7e061bec Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 16:22:21 -0600 Subject: [PATCH 1009/1178] Fixing formatting in parmest --- pyomo/contrib/parmest/parmest.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ae6a6ffe184..c350f315fe4 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -272,7 +272,15 @@ class Estimator(object): # backwards compatible constructor will accept the old deprecated inputs # as well as the new inputs using experiment lists # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor - def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None): + def __init__( + self, + experiment_list, + *args, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): # use deprecated interface self.pest_deprecated = None @@ -283,7 +291,14 @@ def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnos 'please update to the new parmest interface using experiment lists.', version=DEPRECATION_VERSION, ) - self.pest_deprecated = _DeprecatedEstimator(experiment_list, *args, obj_function, tee, diagnostic_mode, solver_options) + self.pest_deprecated = _DeprecatedEstimator( + experiment_list, + *args, + obj_function, + tee, + diagnostic_mode, + solver_options, + ) return # check that we have a (non-empty) list of experiments @@ -309,7 +324,7 @@ def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnos # populate keyword argument options self.obj_function = obj_function self.tee = tee - self.diagnostic_mode = diagnostic_mode + self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options # TODO This might not be needed here. From 8638268d73639866b5f8d885c69efd6db93b5a1d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 16:33:54 -0600 Subject: [PATCH 1010/1178] Fix formatting in parmest --- pyomo/contrib/parmest/parmest.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c350f315fe4..ded40a87aff 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -262,7 +262,7 @@ class Estimator(object): "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. tee: bool, optional - Indicates that ef solver output should be teed + If True, print the solver output to the screen diagnostic_mode: bool, optional If True, print diagnostics from the solver solver_options: dict, optional @@ -273,12 +273,12 @@ class Estimator(object): # as well as the new inputs using experiment lists # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor def __init__( - self, - experiment_list, - *args, - obj_function=None, - tee=False, - diagnostic_mode=False, + self, + experiment_list, + *args, + obj_function=None, + tee=False, + diagnostic_mode=False, solver_options=None, ): @@ -292,11 +292,11 @@ def __init__( version=DEPRECATION_VERSION, ) self.pest_deprecated = _DeprecatedEstimator( - experiment_list, - *args, - obj_function, - tee, - diagnostic_mode, + experiment_list, + *args, + obj_function, + tee, + diagnostic_mode, solver_options, ) return From eeec88ed9f8c550f4f91d9afea155fbf4c218d2b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 18:18:29 -0600 Subject: [PATCH 1011/1178] Reworking parmest Estimator constructor --- pyomo/contrib/parmest/parmest.py | 58 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ded40a87aff..cdff785899e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -53,7 +53,9 @@ import logging import types import json +from collections.abc import Callable from itertools import combinations +from functools import singledispatchmethod from pyomo.common.dependencies import ( attempt_import, @@ -271,39 +273,18 @@ class Estimator(object): # backwards compatible constructor will accept the old deprecated inputs # as well as the new inputs using experiment lists - # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor + @singledispatchmethod def __init__( self, experiment_list, - *args, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None, ): - # use deprecated interface - self.pest_deprecated = None - if callable(experiment_list): - deprecation_warning( - 'Using deprecated parmest interface (model_function, ' - 'data, theta_names). This interface will be removed in a future release, ' - 'please update to the new parmest interface using experiment lists.', - version=DEPRECATION_VERSION, - ) - self.pest_deprecated = _DeprecatedEstimator( - experiment_list, - *args, - obj_function, - tee, - diagnostic_mode, - solver_options, - ) - return - # check that we have a (non-empty) list of experiments assert isinstance(experiment_list, list) - assert len(args) == 0 self.exp_list = experiment_list # check that an experiment has experiment_outputs and unknown_parameters @@ -326,6 +307,9 @@ def __init__( self.tee = tee self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options + self.pest_deprecated = ( + None # TODO: delete this when deprecated interface is removed + ) # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built @@ -339,6 +323,36 @@ def __init__( # boolean to indicate if model is initialized using a square solve self.model_initialized = False + # use deprecated interface + @__init__.register(Callable) + def _deprecated_init( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + + deprecation_warning( + "You're using the deprecated parmest interface (model_function, " + "data, theta_names). This interface will be removed in a future release, " + "please update to the new parmest interface using experiment lists.", + version=DEPRECATION_VERSION, + ) + self.pest_deprecated = _DeprecatedEstimator( + model_function, + data, + theta_names, + obj_function, + tee, + diagnostic_mode, + solver_options, + ) + return + def _return_theta_names(self): """ Return list of fitted model parameter names From add489cb99a8e7ea7d2f65d4fb32662ecb07e57e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 1 May 2024 09:42:45 -0400 Subject: [PATCH 1012/1178] update log of call_before_subproblem_solve --- pyomo/contrib/mindtpy/algorithm_base_class.py | 6 +++--- pyomo/contrib/mindtpy/config_options.py | 4 ++-- pyomo/contrib/mindtpy/single_tree.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 5547350ed44..bbcb8f5fc56 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2950,7 +2950,7 @@ def MindtPy_iteration_loop(self): ) if self.curr_int_sol not in set(self.integer_list): # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(self.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(self.fixed_nlp) fixed_nlp, fixed_nlp_result = self.solve_subproblem() @@ -2965,7 +2965,7 @@ def MindtPy_iteration_loop(self): # The constraint linearization happens in the handlers if not config.solution_pool: # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(self.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(self.fixed_nlp) fixed_nlp, fixed_nlp_result = self.solve_subproblem() @@ -3002,7 +3002,7 @@ def MindtPy_iteration_loop(self): self.integer_list.append(self.curr_int_sol) # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(self.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(self.fixed_nlp) fixed_nlp, fixed_nlp_result = self.solve_subproblem() diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index 019c6933d76..0d0b536525a 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -328,8 +328,8 @@ def _add_common_configs(CONFIG): ConfigValue( default=_DoNothing(), domain=None, - description='Function to be executed after every subproblem', - doc='Callback hook after a solution of the nonlinear subproblem.', + description='Function to be executed before every subproblem', + doc='Callback hook before a solution of the nonlinear subproblem.', ), ) CONFIG.declare( diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index bc0f5d3cf4f..6b501ef874d 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -774,7 +774,7 @@ def __call__(self): # solve subproblem # Call the NLP pre-solve callback - with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() @@ -923,7 +923,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # solve subproblem # Call the NLP pre-solve callback - with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() From dcd5db165e64dcd9b4077401e184266a88719adc Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 09:42:13 -0600 Subject: [PATCH 1013/1178] Adding comments to parmest Estimator constructor logic --- pyomo/contrib/parmest/parmest.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index cdff785899e..3516c52d19d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -271,8 +271,11 @@ class Estimator(object): Provides options to the solver (also the name of an attribute) """ - # backwards compatible constructor will accept the old deprecated inputs - # as well as the new inputs using experiment lists + # The singledispatchmethod decorator is used here as a deprecation + # shim to be able to support the now deprecated Estimator interface + # which had a different number of arguments. When the deprecated API + # is removed this decorator and the _deprecated_init method below + # can be removed @singledispatchmethod def __init__( self, @@ -307,9 +310,9 @@ def __init__( self.tee = tee self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options - self.pest_deprecated = ( - None # TODO: delete this when deprecated interface is removed - ) + + # TODO: delete this when the deprecated interface is removed + self.pest_deprecated = None # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built @@ -323,7 +326,11 @@ def __init__( # boolean to indicate if model is initialized using a square solve self.model_initialized = False - # use deprecated interface + # The deprecated Estimator constructor + # This works by checking the type of the first argument passed to + # the class constructor. If it matches the old interface (i.e. is + # callable) then this _deprecated_init method is called and the + # deprecation warning is displayed. @__init__.register(Callable) def _deprecated_init( self, From 927476913b1c0c6be56ac79c7095ad3414775d38 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 09:48:10 -0600 Subject: [PATCH 1014/1178] Adding default values to the parmest Estimator docstring --- pyomo/contrib/parmest/parmest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 3516c52d19d..c63bb10b89e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -263,12 +263,14 @@ class Estimator(object): If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. + Default is None. tee: bool, optional - If True, print the solver output to the screen + If True, print the solver output to the screen. Default is False. diagnostic_mode: bool, optional - If True, print diagnostics from the solver + If True, print diagnostics from the solver. Default is False. solver_options: dict, optional - Provides options to the solver (also the name of an attribute) + Provides options to the solver (also the name of an attribute). + Default is None. """ # The singledispatchmethod decorator is used here as a deprecation From 48624f4f4198592b32e181c588ac508e7daa0923 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 09:54:31 -0600 Subject: [PATCH 1015/1178] Removing list of parmest TODO items that was opened as a GitHub issue --- pyomo/contrib/parmest/parmest.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c63bb10b89e..f3d35f41013 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -15,16 +15,6 @@ # TODO: move use_mpisppy to a Pyomo configuration option -# Redesign TODOS -# TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) -# TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) -# TODO: Clean up the use of updated_theta_names, model_theta_names, estimator_theta_names. Not sure if estimator_theta_names is the union or intersect of thetas in each model -# TODO: _return_theta_names should no longer be needed -# TODO: generally, theta ordering is not preserved by pyomo, so we should check that ordering -# matches values for each function, otherwise results will be wrong and/or inconsistent -# TODO: return model object (m.k1) and CUIDs in dataframes instead of names ("k1") - - # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: From e01830e117bf27f593fbc49689348c83ae5dc2ea Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 11:35:33 -0600 Subject: [PATCH 1016/1178] Simplify checking for naming conflicts in parmest --- pyomo/contrib/parmest/parmest.py | 35 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index f3d35f41013..28506521524 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -350,14 +350,13 @@ def _deprecated_init( diagnostic_mode, solver_options, ) - return def _return_theta_names(self): """ Return list of fitted model parameter names """ # check for deprecated inputs - if self.pest_deprecated is not None: + if self.pest_deprecated: # if fitted model parameter names differ from theta_names # created when Estimator object is created @@ -365,9 +364,9 @@ def _return_theta_names(self): return self.pest_deprecated.theta_names_updated else: - return ( - self.pest_deprecated.theta_names - ) # default theta_names, created when Estimator object is created + + # default theta_names, created when Estimator object is created + return self.pest_deprecated.theta_names else: @@ -377,9 +376,9 @@ def _return_theta_names(self): return self.theta_names_updated else: - return ( - self.estimator_theta_names - ) # default theta_names, created when Estimator object is created + + # default theta_names, created when Estimator object is created + return self.estimator_theta_names def _expand_indexed_unknowns(self, model_temp): """ @@ -417,21 +416,19 @@ def _create_parmest_model(self, experiment_number): # Add objective function (optional) if self.obj_function: - for obj in model.component_objects(pyo.Objective): - if obj.name in ["Total_Cost_Objective"]: - raise RuntimeError( - "Parmest will not override the existing model Objective named " - + obj.name - ) - obj.deactivate() - for expr in model.component_data_objects(pyo.Expression): - if expr.name in ["FirstStageCost", "SecondStageCost"]: + # Check for component naming conflicts + reserved_names = ['Total_Cost_Objective', 'FirstStageCost', 'SecondStageCost'] + for n in reserved_names: + if model.component(n) or hasattr(model, n): raise RuntimeError( - "Parmest will not override the existing model Expression named " - + expr.name + f"Parmest will not override the existing model component named {n}" ) + # Deactivate any existing objective functions + for obj in model.component_objects(pyo.Objective): + obj.deactivate() + # TODO, this needs to be turned a enum class of options that still support custom functions if self.obj_function == 'SSE': second_stage_rule = SSE From d5a289b884738ee536fb7118517d1c7093dd3ec7 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 12:15:29 -0600 Subject: [PATCH 1017/1178] Simplifying _expand_indexed_unknowns --- pyomo/contrib/parmest/parmest.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 28506521524..1acd63c976d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -384,22 +384,14 @@ def _expand_indexed_unknowns(self, model_temp): """ Expand indexed variables to get full list of thetas """ - model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] - - # check for indexed theta items - indexed_theta_list = [] - for theta_i in model_theta_list: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - for ind in var_validate.index_set(): - if ind is not None: - indexed_theta_list.append(theta_i + '[' + str(ind) + ']') - else: - indexed_theta_list.append(theta_i) - # if we found indexed thetas, use expanded list - if len(indexed_theta_list) > len(model_theta_list): - model_theta_list = indexed_theta_list + model_theta_list = [] + for c in model_temp.unknown_parameters.keys(): + if c.is_indexed(): + for _, ci in c.items(): + model_theta_list.append(ci.name) + else: + model_theta_list.append(c.name) return model_theta_list @@ -418,7 +410,11 @@ def _create_parmest_model(self, experiment_number): if self.obj_function: # Check for component naming conflicts - reserved_names = ['Total_Cost_Objective', 'FirstStageCost', 'SecondStageCost'] + reserved_names = [ + 'Total_Cost_Objective', + 'FirstStageCost', + 'SecondStageCost', + ] for n in reserved_names: if model.component(n) or hasattr(model, n): raise RuntimeError( From 27e84a8fd9d92b2e5e945650fcc8c1f6fb50570d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 16:04:55 -0600 Subject: [PATCH 1018/1178] Cleaning up logic in parmest --- pyomo/contrib/parmest/parmest.py | 34 +++++---------------- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 1acd63c976d..2d4c323b9b8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -390,7 +390,7 @@ def _expand_indexed_unknowns(self, model_temp): if c.is_indexed(): for _, ci in c.items(): model_theta_list.append(ci.name) - else: + else: model_theta_list.append(c.name) return model_theta_list @@ -401,7 +401,6 @@ def _create_parmest_model(self, experiment_number): """ model = self.exp_list[experiment_number].get_labeled_model() - self.theta_names = [k.name for k, v in model.unknown_parameters.items()] if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) @@ -443,29 +442,10 @@ def TotalCost_rule(model): ) # Convert theta Params to Vars, and unfix theta Vars - model = utils.convert_params_to_vars(model, self.theta_names) + theta_names = [k.name for k, v in model.unknown_parameters.items()] + parmest_model = utils.convert_params_to_vars(model, theta_names, fix_vars=False) - # Update theta names list to use CUID string representation - for i, theta in enumerate(self.theta_names): - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(model) - if var_validate is None: - logger.warning( - "theta_name[%s] (%s) was not found on the model", (i, theta) - ) - else: - try: - # If the component is not a variable, - # this will generate an exception (and the warning - # in the 'except') - var_validate.unfix() - self.theta_names[i] = repr(var_cuid) - except: - logger.warning(theta + ' is not a variable') - - self.parmest_model = model - - return model + return parmest_model def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) @@ -1186,12 +1166,14 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) + # TODO: check if model_theta_list is correct if original unknown parameters + # are declared as params and transformed to vars during call to create_parmest_model - # if self.theta_names is not the same as temp model_theta_list, + # if self.estimator_theta_names is not the same as temp model_theta_list, # create self.theta_names_updated if set(self.estimator_theta_names) == set(model_theta_list) and len( self.estimator_theta_names - ) == set(model_theta_list): + ) == len(set(model_theta_list)): pass else: self.theta_names_updated = model_theta_list diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index e9a8e089335..0590f165da3 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -176,7 +176,7 @@ def test_diagnostic_mode(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.theta_names + list(product(asym, rate)), columns=self.pest.estimator_theta_names ) obj_at_theta = self.pest.objective_at_theta(theta_vals) From ae2c5ab44f884dfe637321e4ab07849f5d9d1ce0 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 2 May 2024 17:46:02 -0600 Subject: [PATCH 1019/1178] More parmest cleanup --- pyomo/contrib/parmest/parmest.py | 2 +- pyomo/contrib/parmest/scenariocreator.py | 7 +--- pyomo/contrib/parmest/tests/test_examples.py | 5 ++- pyomo/contrib/parmest/tests/test_utils.py | 36 ++++++++------------ pyomo/contrib/parmest/utils/model_utils.py | 14 ++++++++ 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 2d4c323b9b8..105419dcb13 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -14,7 +14,6 @@ #### Redesign with Experiment class Dec 2023 # TODO: move use_mpisppy to a Pyomo configuration option - # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: @@ -1194,6 +1193,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( theta_temp, model_theta_list ) + assert len(list(theta_names)) == len(model_theta_list) all_thetas = theta_values.to_dict('records') diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 2208bde91a0..7988cfa3f5f 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -169,12 +169,7 @@ def ScenariosFromExperiments(self, addtoSet): opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval + ThetaVals = {k.name: pyo.value(k) for k in model.unknown_parameters.keys()} addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 59a3e0adde2..dca05026e80 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -181,7 +181,10 @@ def test_multisensor_data_example(self): multisensor_data_example.main() - @unittest.skipUnless(matplotlib_available, "test requires matplotlib") + @unittest.skipUnless( + matplotlib_available and seaborn_available, + "test requires matplotlib and seaborn", + ) def test_datarec_example(self): from pyomo.contrib.parmest.examples.reactor_design import datarec_example diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 611d67c1abb..d5e66ab58d5 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -25,18 +25,12 @@ ) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestUtils(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive def test_convert_param_to_var(self): + # TODO: Check that this works for different structured models (indexed, blocks, etc) + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) data = pd.DataFrame( @@ -49,24 +43,22 @@ def test_convert_param_to_var(self): ) # make model - instance = reactor_design_model() - - # add caf, sv - instance.caf = data.iloc[0]['caf'] - instance.sv = data.iloc[0]['sv'] - - solver = pyo.SolverFactory("ipopt") - solver.solve(instance) + exp = ReactorDesignExperiment(data, 0) + instance = exp.get_labeled_model() theta_names = ['k1', 'k2', 'k3'] - instance_vars = parmest.utils.convert_params_to_vars( + m_vars = parmest.utils.convert_params_to_vars( instance, theta_names, fix_vars=True ) - solver.solve(instance_vars) - assert instance.k1() == instance_vars.k1() - assert instance.k2() == instance_vars.k2() - assert instance.k3() == instance_vars.k3() + for v in theta_names: + self.assertTrue(hasattr(m_vars, v)) + c = m_vars.find_component(v) + self.assertIsInstance(c, pyo.Var) + self.assertTrue(c.fixed) + c_old = instance.find_component(v) + self.assertEqual(pyo.value(c), pyo.value(c_old)) + self.assertTrue(c in m_vars.unknown_parameters) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index 77491f74b02..7778ebcc9f1 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -15,6 +15,7 @@ from pyomo.core.expr import replace_expressions, identify_mutable_parameters from pyomo.core.base.var import IndexedVar from pyomo.core.base.param import IndexedParam +from pyomo.common.collections import ComponentMap from pyomo.environ import ComponentUID @@ -49,6 +50,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # Convert Params to Vars, unfix Vars, and create a substitution map substitution_map = {} + comp_map = ComponentMap() for i, param_name in enumerate(param_names): # Leverage the parser in ComponentUID to locate the component. theta_cuid = ComponentUID(param_name) @@ -65,6 +67,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): theta_var_cuid = ComponentUID(theta_object.name) theta_var_object = theta_var_cuid.find_component_on(model) substitution_map[id(theta_object)] = theta_var_object + comp_map[theta_object] = theta_var_object # Indexed Param elif isinstance(theta_object, IndexedParam): @@ -90,6 +93,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # Update substitution map (map each indexed param to indexed var) theta_var_cuid = ComponentUID(theta_object.name) theta_var_object = theta_var_cuid.find_component_on(model) + comp_map[theta_object] = theta_var_object var_theta_objects = [] for theta_obj in theta_var_object: theta_cuid = ComponentUID( @@ -101,6 +105,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): param_theta_objects, var_theta_objects ): substitution_map[id(param_theta_obj)] = var_theta_obj + comp_map[param_theta_obj] = var_theta_obj # Var or Indexed Var elif isinstance(theta_object, IndexedVar) or theta_object.is_variable_type(): @@ -182,6 +187,15 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): model.del_component(obj) model.add_component(obj.name, pyo.Objective(rule=expr, sense=obj.sense)) + # Convert Params to Vars in Suffixes + for s in model.component_objects(pyo.Suffix): + current_keys = list(s.keys()) + for c in current_keys: + if c in comp_map: + s[comp_map[c]] = s.pop(c) + + assert len(current_keys) == len(s.keys()) + # print('--- Updated Model ---') # model.pprint() # solver = pyo.SolverFactory('ipopt') From 88c12276c2d314eb01b3e0569ba5efbb29e41cf3 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 2 May 2024 18:09:32 -0600 Subject: [PATCH 1020/1178] Remove fixed TODO and fix typo in parmest test --- pyomo/contrib/parmest/parmest.py | 2 -- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 105419dcb13..41e1724f94f 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1165,8 +1165,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) - # TODO: check if model_theta_list is correct if original unknown parameters - # are declared as params and transformed to vars during call to create_parmest_model # if self.estimator_theta_names is not the same as temp model_theta_list, # create self.theta_names_updated diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 0590f165da3..5f288154dcd 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -135,7 +135,7 @@ def test_likelihood_ratio(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.theta_names + list(product(asym, rate)), columns=self.pest.estimator_theta_names ) obj_at_theta = self.pest.objective_at_theta(theta_vals) From 9cd9986227f23149dc814d731725da87cb6f958d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:51:52 -0600 Subject: [PATCH 1021/1178] Resolve (and test) error in RenamedClass when derived classes have multiple bases --- pyomo/common/deprecation.py | 2 +- pyomo/common/tests/test_deprecated.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 5a6ca456079..c674dcddc78 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -542,7 +542,7 @@ def __renamed__warning__(msg): if new_class is None and '__renamed__new_class__' not in classdict: if not any( - hasattr(base, '__renamed__new_class__') + hasattr(mro, '__renamed__new_class__') for mro in itertools.chain.from_iterable( base.__mro__ for base in renamed_bases ) diff --git a/pyomo/common/tests/test_deprecated.py b/pyomo/common/tests/test_deprecated.py index 377e229c775..37e1ba81bb3 100644 --- a/pyomo/common/tests/test_deprecated.py +++ b/pyomo/common/tests/test_deprecated.py @@ -529,7 +529,10 @@ class DeprecatedClassSubclass(DeprecatedClass): out = StringIO() with LoggingIntercept(out): - class DeprecatedClassSubSubclass(DeprecatedClassSubclass): + class otherClass: + pass + + class DeprecatedClassSubSubclass(DeprecatedClassSubclass, otherClass): attr = 'DeprecatedClassSubSubclass' self.assertEqual(out.getvalue(), "") From 79e2e3650d49fb4d29bb65cd39a4edbe35b6835b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:53:34 -0600 Subject: [PATCH 1022/1178] Resolve backwards compatibility from renaming / removing pyomo_constant_types import --- pyomo/core/expr/numvalue.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index b656eea1bcd..3b335bd5fc4 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -44,6 +44,16 @@ "be treated as if they were bool (as was the case for the other " "native_*_types sets). Users likely should use native_logical_types.", ) +relocated_module_attribute( + 'pyomo_constant_types', + 'pyomo.common.numeric_types._pyomo_constant_types', + version='6.7.2.dev0', + f_globals=globals(), + msg="The pyomo_constant_types set will be removed in the future: the set " + "contained only NumericConstant and _PythonCallbackFunctionID, and provided " + "no meaningful value to clients or walkers. Users should likely handle " + "these types in the same manner as immutable Params.", +) relocated_module_attribute( 'RegisterNumericType', 'pyomo.common.numeric_types.RegisterNumericType', From 2d818adbfc0629d07a1111f66accd90d543eacdf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:54:15 -0600 Subject: [PATCH 1023/1178] Overhaul declare_custom_block to avoid using metaclasses --- pyomo/core/base/block.py | 87 ++++++++++++++--------------- pyomo/core/tests/unit/test_block.py | 31 ++++++++++ 2 files changed, 73 insertions(+), 45 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 3eb18dde7a9..a27ca81bbfd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2333,48 +2333,42 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F BlockData._Block_reserved_words = set(dir(Block())) -class _IndexedCustomBlockMeta(type): - """Metaclass for creating an indexed custom block.""" - - pass - - -class _ScalarCustomBlockMeta(type): - """Metaclass for creating a scalar custom block.""" - - def __new__(meta, name, bases, dct): - def __init__(self, *args, **kwargs): - # bases[0] is the custom block data object - bases[0].__init__(self, component=self) - # bases[1] is the custom block object that - # is used for declaration - bases[1].__init__(self, *args, **kwargs) - - dct["__init__"] = __init__ - return type.__new__(meta, name, bases, dct) +class ScalarCustomBlockMixin(object): + def __init__(self, *args, **kwargs): + # __bases__ for the ScalarCustomBlock is + # + # (ScalarCustomBlockMixin, {custom_data}, {custom_block}) + # + # Unfortunately, we cannot guarantee that this is being called + # from the ScalarCustomBlock (someone could have inherited from + # that class to make another scalar class). We will walk up the + # MRO to find the Scalar class (which should be the only class + # that has this Mixin as the first base class) + for cls in self.__class__.__mro__: + if cls.__bases__[0] is ScalarCustomBlockMixin: + _mixin, _data, _block = cls.__bases__ + _data.__init__(self, component=self) + _block.__init__(self, *args, **kwargs) + break class CustomBlock(Block): """The base class used by instances of custom block components""" - def __init__(self, *args, **kwds): + def __init__(self, *args, **kwargs): if self._default_ctype is not None: - kwds.setdefault('ctype', self._default_ctype) - Block.__init__(self, *args, **kwds) + kwargs.setdefault('ctype', self._default_ctype) + Block.__init__(self, *args, **kwargs) - def __new__(cls, *args, **kwds): - if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'): + def __new__(cls, *args, **kwargs): + if cls.__bases__[0] is not CustomBlock: # we are entering here the second time (recursive) # therefore, we need to create what we have - return super(CustomBlock, cls).__new__(cls) + return super().__new__(cls, *args, **kwargs) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): - n = _ScalarCustomBlockMeta( - "_Scalar%s" % (cls.__name__,), (cls._ComponentDataClass, cls), {} - ) - return n.__new__(n) + return super().__new__(cls._scalar_custom_block, *args, **kwargs) else: - n = _IndexedCustomBlockMeta("_Indexed%s" % (cls.__name__,), (cls,), {}) - return n.__new__(n) + return super().__new__(cls._indexed_custom_block, *args, **kwargs) def declare_custom_block(name, new_ctype=None): @@ -2386,9 +2380,9 @@ def declare_custom_block(name, new_ctype=None): ... pass """ - def proc_dec(cls): - # this is the decorator function that - # creates the block component class + def block_data_decorator(cls): + # this is the decorator function that creates the block + # component classes # Default (derived) Block attributes clsbody = { @@ -2399,7 +2393,7 @@ def proc_dec(cls): "_default_ctype": None, } - c = type( + c = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes clsbody, # class body definitions (will populate __dict__) @@ -2408,7 +2402,7 @@ def proc_dec(cls): if new_ctype is not None: if new_ctype is True: c._default_ctype = c - elif type(new_ctype) is type: + elif isinstance(new_ctype, type): c._default_ctype = new_ctype else: raise ValueError( @@ -2416,15 +2410,18 @@ def proc_dec(cls): "or 'True'; received: %s" % (new_ctype,) ) - # Register the new Block type in the same module as the BlockData - setattr(sys.modules[cls.__module__], name, c) - # TODO: can we also register concrete Indexed* and Scalar* - # classes into the original BlockData module (instead of relying - # on metaclasses)? + # Declare Indexed and Scalar versions of the custom blocks. We + # will register them both with the calling module scope, and + # with the CustomBlock (so that __new__ can route the object + # creation to the correct class) + c._indexed_custom_block = type(c)("Indexed" + name, (c,), {}) + c._scalar_custom_block = type(c)( + "Scalar" + name, (ScalarCustomBlockMixin, cls, c), {} + ) - # are these necessary? - setattr(cls, '_orig_name', name) - setattr(cls, '_orig_module', cls.__module__) + # Register the new Block types in the same module as the BlockData + for _cls in (c, c._indexed_custom_block, c._scalar_custom_block): + setattr(sys.modules[cls.__module__], _cls.__name__, _cls) return cls - return proc_dec + return block_data_decorator diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 660f65f1944..33d6d2c8adc 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -13,6 +13,7 @@ # from io import StringIO +import logging import os import sys import types @@ -2975,6 +2976,36 @@ def test_write_exceptions(self): with self.assertRaisesRegex(ValueError, ".*Cannot write model in format"): m.write(format="bogus") + def test_custom_block(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + def __init__(self, component): + BlockData.__init__(self, component) + logging.getLogger(__name__).warning("TestingBlockData.__init__") + + self.assertIn('TestingBlock', globals()) + self.assertIn('ScalarTestingBlock', globals()) + self.assertIn('IndexedTestingBlock', globals()) + + with LoggingIntercept() as LOG: + obj = TestingBlock() + self.assertIs(type(obj), ScalarTestingBlock) + self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") + + with LoggingIntercept() as LOG: + obj = TestingBlock([1, 2]) + self.assertIs(type(obj), IndexedTestingBlock) + self.assertEqual(LOG.getvalue(), "") + + # Test that we can derive from a ScalarCustomBlock + class DerivedScalarTstingBlock(ScalarTestingBlock): + pass + + with LoggingIntercept() as LOG: + obj = DerivedScalarTstingBlock() + self.assertIs(type(obj), DerivedScalarTstingBlock) + self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") + def test_override_pprint(self): @declare_custom_block('TempBlock') class TempBlockData(BlockData): From e4d26b3e37d2f074e58d5e272ea60a2c1604fada Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:55:00 -0600 Subject: [PATCH 1024/1178] NFC: clarify comment --- pyomo/core/base/block.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index a27ca81bbfd..5513d405f4d 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2412,8 +2412,8 @@ def block_data_decorator(cls): # Declare Indexed and Scalar versions of the custom blocks. We # will register them both with the calling module scope, and - # with the CustomBlock (so that __new__ can route the object - # creation to the correct class) + # with the CustomBlock (so that CustomBlock.__new__ can route + # the object creation to the correct class) c._indexed_custom_block = type(c)("Indexed" + name, (c,), {}) c._scalar_custom_block = type(c)( "Scalar" + name, (ScalarCustomBlockMixin, cls, c), {} From cff93a17664803915b0d0ea4d3bdf9f675a3a1d7 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 14:11:47 -0600 Subject: [PATCH 1025/1178] Fixing non deterministic fragile test in parmest --- pyomo/contrib/parmest/tests/test_parmest.py | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 5f288154dcd..69155dadb45 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -118,9 +118,9 @@ def test_bootstrap(self): CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) - self.assertTrue(CR[0.5].sum() == 5) - self.assertTrue(CR[0.75].sum() == 7) - self.assertTrue(CR[1.0].sum() == 10) # all true + self.assertEqual(CR[0.5].sum(), 5) + self.assertEqual(CR[0.75].sum(), 7) + self.assertEqual(CR[1.0].sum(), 10) # all true graphics.pairwise_plot(theta_est) graphics.pairwise_plot(theta_est, thetavals) @@ -135,17 +135,16 @@ def test_likelihood_ratio(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.estimator_theta_names + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] ) - obj_at_theta = self.pest.objective_at_theta(theta_vals) LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) - self.assertTrue(LR[0.8].sum() == 6) - self.assertTrue(LR[0.9].sum() == 10) - self.assertTrue(LR[1.0].sum() == 60) # all true + self.assertEqual(LR[0.8].sum(), 6) + self.assertEqual(LR[0.9].sum(), 10) + self.assertEqual(LR[1.0].sum(), 60) # all true graphics.pairwise_plot(LR, thetavals, 0.8) @@ -164,9 +163,9 @@ def test_leaveNout(self): self.assertTrue(samples == [1]) # sample 1 was left out self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) - self.assertTrue(lno_theta[1.0].sum() == 1) # all true - self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 - self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true + self.assertEqual(lno_theta[1.0].sum(), 1) # all true + self.assertEqual(bootstrap_theta.shape[0], 3) # bootstrap for sample 1 + self.assertEqual(bootstrap_theta[1.0].sum(), 3) # all true def test_diagnostic_mode(self): self.pest.diagnostic_mode = True @@ -176,7 +175,7 @@ def test_diagnostic_mode(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.estimator_theta_names + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] ) obj_at_theta = self.pest.objective_at_theta(theta_vals) From cefb6d84a0a117ecb198c682b00465a924c00163 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 14:30:18 -0600 Subject: [PATCH 1026/1178] Fixing typo in class name --- pyomo/core/tests/unit/test_block.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 33d6d2c8adc..063db0e428e 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2998,12 +2998,12 @@ def __init__(self, component): self.assertEqual(LOG.getvalue(), "") # Test that we can derive from a ScalarCustomBlock - class DerivedScalarTstingBlock(ScalarTestingBlock): + class DerivedScalarTestingBlock(ScalarTestingBlock): pass with LoggingIntercept() as LOG: - obj = DerivedScalarTstingBlock() - self.assertIs(type(obj), DerivedScalarTstingBlock) + obj = DerivedScalarTestingBlock() + self.assertIs(type(obj), DerivedScalarTestingBlock) self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") def test_override_pprint(self): From e96b382392eef69a5feac0d85c6593486e7e2d42 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 15:11:13 -0600 Subject: [PATCH 1027/1178] Try relaxing gurobipy version --- .github/workflows/test_branches.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 75db5d66431..3396eca0176 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -111,6 +111,14 @@ jobs: TARGET: win PYENV: pip + - os: ubuntu-latest + python: '3.11' + other: /singletest + category: "-m 'neos or importtest'" + skip_doctest: 1 + TARGET: linux + PYENV: pip + steps: - name: Checkout Pyomo source uses: actions/checkout@v4 @@ -265,7 +273,7 @@ jobs: python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy==10.0.3 \ + -i https://pypi.gurobi.com gurobipy \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From 151c4aa10364be25deb2427950c0ead38cc549eb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 15:24:01 -0600 Subject: [PATCH 1028/1178] Ensure all custom block classes are assigned to the module scope --- pyomo/core/base/block.py | 22 ++++++++++++++++------ pyomo/core/tests/unit/test_block.py | 3 +++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 5513d405f4d..376ed30e1dd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2380,13 +2380,13 @@ def declare_custom_block(name, new_ctype=None): ... pass """ - def block_data_decorator(cls): + def block_data_decorator(block_data): # this is the decorator function that creates the block # component classes # Default (derived) Block attributes clsbody = { - "__module__": cls.__module__, # magic to fix the module + "__module__": block_data.__module__, # magic to fix the module # Default IndexedComponent data object is the decorated class: "_ComponentDataClass": cls, # By default this new block does not declare a new ctype @@ -2414,14 +2414,24 @@ def block_data_decorator(cls): # will register them both with the calling module scope, and # with the CustomBlock (so that CustomBlock.__new__ can route # the object creation to the correct class) - c._indexed_custom_block = type(c)("Indexed" + name, (c,), {}) + c._indexed_custom_block = type(c)( + "Indexed" + name, + (c,), + { # ensure the created class is associated with the calling module + "__module__": block_data.__module__ + }, + ) c._scalar_custom_block = type(c)( - "Scalar" + name, (ScalarCustomBlockMixin, cls, c), {} + "Scalar" + name, + (ScalarCustomBlockMixin, block_data, c), + { # ensure the created class is associated with the calling module + "__module__": block_data.__module__ + }, ) # Register the new Block types in the same module as the BlockData for _cls in (c, c._indexed_custom_block, c._scalar_custom_block): - setattr(sys.modules[cls.__module__], _cls.__name__, _cls) - return cls + setattr(sys.modules[block_data.__module__], _cls.__name__, _cls) + return block_data return block_data_decorator diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 063db0e428e..bf4a5d58636 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2986,6 +2986,9 @@ def __init__(self, component): self.assertIn('TestingBlock', globals()) self.assertIn('ScalarTestingBlock', globals()) self.assertIn('IndexedTestingBlock', globals()) + self.assertIs(TestingBlock.__module__, __name__) + self.assertIs(ScalarTestingBlock.__module__, __name__) + self.assertIs(IndexedTestingBlock.__module__, __name__) with LoggingIntercept() as LOG: obj = TestingBlock() From 04a9708e169b6f417d6a470b14b6cb38b9d756b3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 15:24:26 -0600 Subject: [PATCH 1029/1178] Improve documentation --- pyomo/core/base/block.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 376ed30e1dd..72d66c67c60 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2362,9 +2362,16 @@ def __init__(self, *args, **kwargs): def __new__(cls, *args, **kwargs): if cls.__bases__[0] is not CustomBlock: - # we are entering here the second time (recursive) - # therefore, we need to create what we have + # we are creating a class other than the "generic" derived + # custom block class. We can assume that the routing of the + # generic block class to the specific Scalar or Indexed + # subclass has already occurred and we can pass control up + # to (toward) object.__new__() return super().__new__(cls, *args, **kwargs) + # If the first base class is this CustomBlock class, then the + # user is attempting to create the "generic" block class. + # Depending on the arguments, we need to map this to either the + # Scalar or Indexed block subclass. if not args or (args[0] is UnindexedComponent_set and len(args) == 1): return super().__new__(cls._scalar_custom_block, *args, **kwargs) else: @@ -2374,7 +2381,7 @@ def __new__(cls, *args, **kwargs): def declare_custom_block(name, new_ctype=None): """Decorator to declare components for a custom block data class - >>> @declare_custom_block(name=FooBlock) + >>> @declare_custom_block(name="FooBlock") ... class FooBlockData(BlockData): ... # custom block data class ... pass @@ -2384,19 +2391,24 @@ def block_data_decorator(block_data): # this is the decorator function that creates the block # component classes - # Default (derived) Block attributes - clsbody = { - "__module__": block_data.__module__, # magic to fix the module - # Default IndexedComponent data object is the decorated class: - "_ComponentDataClass": cls, - # By default this new block does not declare a new ctype - "_default_ctype": None, - } - + # Declare the new Block (derived from CustomBlock) corresponding + # to the BlockData that we are decorating + # + # Note the use of `type(CustomBlock)` to pick up the metaclass + # that was used to create the CustomBlock (in general, it should + # be `type`) c = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes - clsbody, # class body definitions (will populate __dict__) + # class body definitions (populate the new class' __dict__) + { + # ensure the created class is associated with the calling module + "__module__": block_data.__module__, + # Default IndexedComponent data object is the decorated class: + "_ComponentDataClass": block_data, + # By default this new block does not declare a new ctype + "_default_ctype": None, + }, ) if new_ctype is not None: @@ -2410,7 +2422,7 @@ def block_data_decorator(block_data): "or 'True'; received: %s" % (new_ctype,) ) - # Declare Indexed and Scalar versions of the custom blocks. We + # Declare Indexed and Scalar versions of the custom block. We # will register them both with the calling module scope, and # with the CustomBlock (so that CustomBlock.__new__ can route # the object creation to the correct class) From a934e9f86540058d9a81a0989432458cb702521f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 15:28:53 -0600 Subject: [PATCH 1030/1178] Try grabbing gurobipy straight from pypi --- .github/workflows/test_branches.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3396eca0176..dc0bdecff18 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -272,8 +272,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy \ + python -m pip install --cache-dir cache/pip gurobipy\ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From fea6ca8f6cd62491acedb89c130e7749a26693b7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 15:34:55 -0600 Subject: [PATCH 1031/1178] Improve variable naming --- pyomo/core/base/block.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 72d66c67c60..26f2d7071b1 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2391,13 +2391,13 @@ def block_data_decorator(block_data): # this is the decorator function that creates the block # component classes - # Declare the new Block (derived from CustomBlock) corresponding - # to the BlockData that we are decorating + # Declare the new Block component (derived from CustomBlock) + # corresponding to the BlockData that we are decorating # # Note the use of `type(CustomBlock)` to pick up the metaclass # that was used to create the CustomBlock (in general, it should # be `type`) - c = type(CustomBlock)( + comp = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes # class body definitions (populate the new class' __dict__) @@ -2413,9 +2413,9 @@ def block_data_decorator(block_data): if new_ctype is not None: if new_ctype is True: - c._default_ctype = c + comp._default_ctype = comp elif isinstance(new_ctype, type): - c._default_ctype = new_ctype + comp._default_ctype = new_ctype else: raise ValueError( "Expected new_ctype to be either type " @@ -2426,23 +2426,23 @@ def block_data_decorator(block_data): # will register them both with the calling module scope, and # with the CustomBlock (so that CustomBlock.__new__ can route # the object creation to the correct class) - c._indexed_custom_block = type(c)( + comp._indexed_custom_block = type(comp)( "Indexed" + name, - (c,), + (comp,), { # ensure the created class is associated with the calling module "__module__": block_data.__module__ }, ) - c._scalar_custom_block = type(c)( + comp._scalar_custom_block = type(comp)( "Scalar" + name, - (ScalarCustomBlockMixin, block_data, c), + (ScalarCustomBlockMixin, block_data, comp), { # ensure the created class is associated with the calling module "__module__": block_data.__module__ }, ) # Register the new Block types in the same module as the BlockData - for _cls in (c, c._indexed_custom_block, c._scalar_custom_block): + for _cls in (comp, comp._indexed_custom_block, comp._scalar_custom_block): setattr(sys.modules[block_data.__module__], _cls.__name__, _cls) return block_data From 6d94224f965fa1415f8da78da974d50df9d58046 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 15:56:03 -0600 Subject: [PATCH 1032/1178] Repinning gurobipy version --- .github/workflows/test_branches.yml | 10 +--------- .github/workflows/test_pr_and_main.yml | 3 +-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index dc0bdecff18..611875fb456 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -111,14 +111,6 @@ jobs: TARGET: win PYENV: pip - - os: ubuntu-latest - python: '3.11' - other: /singletest - category: "-m 'neos or importtest'" - skip_doctest: 1 - TARGET: linux - PYENV: pip - steps: - name: Checkout Pyomo source uses: actions/checkout@v4 @@ -272,7 +264,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip gurobipy\ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3\ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 5a484dccbc8..5b1bca70ede 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -301,8 +301,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy==10.0.3 \ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From 97b3b0a9efda63e1021d038de1430cd021692bc7 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 17:16:21 -0600 Subject: [PATCH 1033/1178] Minor edits to scenariocreator in parmest --- pyomo/contrib/parmest/scenariocreator.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 7988cfa3f5f..1729c6e6c72 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,11 +14,6 @@ import pyomo.environ as pyo -from pyomo.common.deprecation import deprecated -from pyomo.common.deprecation import deprecation_warning - -DEPRECATION_VERSION = '6.7.2.dev0' - import logging logger = logging.getLogger(__name__) @@ -129,14 +124,9 @@ class ScenarioCreator(object): def __init__(self, pest, solvername): - # is this a deprecated pest object? + # Check if we're using the deprecated parmest API self.scen_deprecated = None if pest.pest_deprecated is not None: - deprecation_warning( - "Using a deprecated parmest object for scenario " - + "creator, please recreate object using experiment lists.", - version=DEPRECATION_VERSION, - ) self.scen_deprecated = _ScenarioCreatorDeprecated( pest.pest_deprecated, solvername ) @@ -218,7 +208,7 @@ def ScenariosFromExperiments(self, addtoSet): a ScenarioSet """ - # assert isinstance(addtoSet, ScenarioSet) + assert isinstance(addtoSet, ScenarioSet) scenario_numbers = list(range(len(self.pest.callback_data))) @@ -247,7 +237,7 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): numtomake (int) : number of scenarios to create """ - # assert isinstance(addtoSet, ScenarioSet) + assert isinstance(addtoSet, ScenarioSet) bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) From ff26f636f2d312505bd67822efbf7504d38b6445 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 17:48:00 -0600 Subject: [PATCH 1034/1178] Simplify scenariocreator code in parmest to remove duplication --- pyomo/contrib/parmest/scenariocreator.py | 108 ++++++----------------- 1 file changed, 25 insertions(+), 83 deletions(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 1729c6e6c72..e887dd2e8be 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -124,78 +124,6 @@ class ScenarioCreator(object): def __init__(self, pest, solvername): - # Check if we're using the deprecated parmest API - self.scen_deprecated = None - if pest.pest_deprecated is not None: - self.scen_deprecated = _ScenarioCreatorDeprecated( - pest.pest_deprecated, solvername - ) - else: - self.pest = pest - self.solvername = solvername - - def ScenariosFromExperiments(self, addtoSet): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - Returns: - a ScenarioSet - """ - - # check if using deprecated pest object - if self.scen_deprecated is not None: - self.scen_deprecated.ScenariosFromExperiments(addtoSet) - return - - assert isinstance(addtoSet, ScenarioSet) - - scenario_numbers = list(range(len(self.pest.exp_list))) - - prob = 1.0 / len(scenario_numbers) - for exp_num in scenario_numbers: - ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback(exp_num) - opt = pyo.SolverFactory(self.solvername) - results = opt.solve(model) # solves and updates model - ## pyo.check_termination_optimal(results) - ThetaVals = {k.name: pyo.value(k) for k in model.unknown_parameters.keys()} - addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) - - def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - numtomake (int) : number of scenarios to create - """ - - # check if using deprecated pest object - if self.scen_deprecated is not None: - self.scen_deprecated.ScenariosFromBootstrap(addtoSet, numtomake, seed=seed) - return - - assert isinstance(addtoSet, ScenarioSet) - - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) - addtoSet.append_bootstrap(bootstrap_thetas) - - -################################ -# deprecated functions/classes # -################################ - - -class _ScenarioCreatorDeprecated(object): - """Create scenarios from parmest. - - Args: - pest (Estimator): the parmest object - solvername (str): name of the solver (e.g. "ipopt") - - """ - - def __init__(self, pest, solvername): self.pest = pest self.solvername = solvername @@ -210,23 +138,32 @@ def ScenariosFromExperiments(self, addtoSet): assert isinstance(addtoSet, ScenarioSet) - scenario_numbers = list(range(len(self.pest.callback_data))) + if self.pest.pest_deprecated is not None: + scenario_numbers = list(range(len(self.pest.pest_deprecated.callback_data))) + else: + scenario_numbers = list(range(len(self.pest.exp_list))) prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data - ) + if self.pest.pest_deprecated is not None: + model = self.pest.pest_deprecated._instance_creation_callback( + exp_num, self.pest.pest_deprecated.callback_data + ) + else: + model = self.pest._instance_creation_callback(exp_num) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval + if self.pest.pest_deprecated is not None: + ThetaVals = { + theta: pyo.value(model.find_component(theta)) + for theta in self.pest.pest_deprecated.theta_names + } + else: + ThetaVals = { + k.name: pyo.value(k) for k in model.unknown_parameters.keys() + } addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): @@ -239,5 +176,10 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): assert isinstance(addtoSet, ScenarioSet) - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + if self.pest.pest_deprecated is not None: + bootstrap_thetas = self.pest.pest_deprecated.theta_est_bootstrap( + numtomake, seed=seed + ) + else: + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) From e13edeea1401b4d105c795cff610557321291c95 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 22:14:07 -0600 Subject: [PATCH 1035/1178] Test ctype management in declare_custom_block() --- pyomo/core/tests/unit/test_block.py | 30 ++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index bf4a5d58636..3d578f7dc88 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3009,7 +3009,35 @@ class DerivedScalarTestingBlock(ScalarTestingBlock): self.assertIs(type(obj), DerivedScalarTestingBlock) self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") - def test_override_pprint(self): + def test_custom_block_ctypes(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Block) + + @declare_custom_block('TestingBlock', True) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, TestingBlock) + + @declare_custom_block('TestingBlock', Constraint) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Constraint) + + with self.assertRaisesRegex( + ValueError, + r"Expected new_ctype to be either type or 'True'; received: \[\]", + ): + + @declare_custom_block('TestingBlock', []) + class TestingBlockData(BlockData): + pass + + def test_custom_block_override_pprint(self): @declare_custom_block('TempBlock') class TempBlockData(BlockData): def pprint(self, ostream=None, verbose=False, prefix=""): From 54bd3d393b73d1dcfa6bdec1854b69893eaec679 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 10:08:04 -0600 Subject: [PATCH 1036/1178] Move the ginac interface sources to a subdirectory --- pyomo/contrib/simplification/build.py | 9 ++-- .../contrib/simplification/ginac/__init__.py | 52 +++++++++++++++++++ .../{ => ginac/src}/ginac_interface.cpp | 0 .../{ => ginac/src}/ginac_interface.hpp | 0 pyomo/contrib/simplification/simplify.py | 14 ++--- 5 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 pyomo/contrib/simplification/ginac/__init__.py rename pyomo/contrib/simplification/{ => ginac/src}/ginac_interface.cpp (100%) rename pyomo/contrib/simplification/{ => ginac/src}/ginac_interface.hpp (100%) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index a4094f993fa..0ae883bc55c 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -88,9 +88,10 @@ def build_ginac_interface(parallel=None, args=None): if args is None: args = [] - dname = this_file_dir() - _sources = ['ginac_interface.cpp'] - sources = [os.path.join(dname, fname) for fname in _sources] + sources = [ + os.path.join(this_file_dir(), 'ginac', 'src', fname) + for fname in ['ginac_interface.cpp'] + ] ginac_lib = find_library('ginac') if not ginac_lib: @@ -132,7 +133,7 @@ def run(self): basedir = os.path.abspath(os.path.curdir) with TempfileManager.new_context() as tempfile: if self.inplace: - tmpdir = this_file_dir() + tmpdir = os.path.join(this_file_dir(), 'ginac') else: tmpdir = os.path.abspath(tempfile.mkdtemp()) sys.stdout.write("Building in '%s'" % tmpdir) diff --git a/pyomo/contrib/simplification/ginac/__init__.py b/pyomo/contrib/simplification/ginac/__init__.py new file mode 100644 index 00000000000..af6511944de --- /dev/null +++ b/pyomo/contrib/simplification/ginac/__init__.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import attempt_import as _attempt_import + + +def _importer(): + import os + import sys + from ctypes import cdll + from pyomo.common.envvar import PYOMO_CONFIG_DIR + from pyomo.common.fileutils import find_library + + try: + pyomo_config_dir = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + ) + sys.path.insert(0, pyomo_config_dir) + # GiNaC needs 2 libraries that are generally dynamically linked + # to the interface library. If we built those ourselves, then + # the libraries will be PYOMO_CONFIG_DIR/lib ... but that + # directlor is very likely to NOT be on the library search path + # when the Python interpreter was started. We will manually + # look for those two libraries, and if we find them, load them + # into this process (so the interface can find them) + for lib in ('cln', 'ginac'): + fname = find_library(lib) + if fname is not None: + cdll.LoadLibrary(fname) + + import ginac_interface + except ImportError: + from . import ginac_interface + finally: + assert sys.path[0] == pyomo_config_dir + sys.path.pop(0) + + return ginac_interface + + +interface, interface_available = _attempt_import('ginac_interface', importer=_importer) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp similarity index 100% rename from pyomo/contrib/simplification/ginac_interface.cpp rename to pyomo/contrib/simplification/ginac/src/ginac_interface.cpp diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.hpp similarity index 100% rename from pyomo/contrib/simplification/ginac_interface.hpp rename to pyomo/contrib/simplification/ginac/src/ginac_interface.hpp diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 27da5f5ca34..94f0ceaa33f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -15,14 +15,10 @@ import logging import warnings -try: - from pyomo.contrib.simplification.ginac_interface import GinacInterface - - ginac_available = True -except: - GinacInterface = None - ginac_available = False - +from pyomo.contrib.simplification.ginac import ( + interface as ginac_interface, + interface_available as ginac_available, +) logger = logging.getLogger(__name__) @@ -51,7 +47,7 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: if ginac_available: - self.gi = GinacInterface(False) + self.gi = ginac_interface.GinacInterface(False) self.suppress_no_ginac_warnings = suppress_no_ginac_warnings def simplify(self, expr: NumericExpression): From 8a37c8b1e0a4a54702eb81996d5730a4e55c0da5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 10:08:50 -0600 Subject: [PATCH 1037/1178] Update builder to use argparse, clean up output --- pyomo/contrib/simplification/build.py | 73 +++++++++++++++------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 0ae883bc55c..d540991b010 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -21,12 +21,11 @@ from pyomo.common.fileutils import find_library, this_file_dir from pyomo.common.tempfiles import TempfileManager +logger = logging.getLogger(__name__ if __name__ != '__main__' else 'pyomo') -logger = logging.getLogger(__name__) - -def build_ginac_library(parallel=None, argv=None): - sys.stdout.write("\n**** Building GiNaC library ****") +def build_ginac_library(parallel=None, argv=None, env=None): + sys.stdout.write("\n**** Building GiNaC library ****\n") configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] make_cmd = ['make'] @@ -34,6 +33,12 @@ def build_ginac_library(parallel=None, argv=None): make_cmd.append(f'-j{parallel}') install_cmd = ['make', 'install'] + env = dict(os.environ) + pcdir = os.path.join(PYOMO_CONFIG_DIR, 'lib', 'pkgconfig') + if 'PKG_CONFIG_PATH' in env: + pcdir += os.pathsep + env['PKG_CONFIG_PATH'] + env['PKG_CONFIG_PATH'] = pcdir + with TempfileManager.new_context() as tempfile: tmpdir = tempfile.mkdtemp() @@ -49,10 +54,10 @@ def build_ginac_library(parallel=None, argv=None): % (url, downloader.destination()) ) downloader.get_tar_archive(url, dirOffset=1) - assert subprocess.run(configure_cmd, cwd=cln_dir).returncode == 0 + assert subprocess.run(configure_cmd, cwd=cln_dir, env=env).returncode == 0 logger.info("\nBuilding CLN\n") - assert subprocess.run(make_cmd, cwd=cln_dir).returncode == 0 - assert subprocess.run(install_cmd, cwd=cln_dir).returncode == 0 + assert subprocess.run(make_cmd, cwd=cln_dir, env=env).returncode == 0 + assert subprocess.run(install_cmd, cwd=cln_dir, env=env).returncode == 0 url = 'https://www.ginac.de/ginac-1.8.7.tar.bz2' ginac_dir = os.path.join(tmpdir, 'ginac') @@ -62,10 +67,10 @@ def build_ginac_library(parallel=None, argv=None): % (url, downloader.destination()) ) downloader.get_tar_archive(url, dirOffset=1) - assert subprocess.run(configure_cmd, cwd=ginac_dir).returncode == 0 + assert subprocess.run(configure_cmd, cwd=ginac_dir, env=env).returncode == 0 logger.info("\nBuilding GiNaC\n") - assert subprocess.run(make_cmd, cwd=ginac_dir).returncode == 0 - assert subprocess.run(install_cmd, cwd=ginac_dir).returncode == 0 + assert subprocess.run(make_cmd, cwd=ginac_dir, env=env).returncode == 0 + assert subprocess.run(install_cmd, cwd=ginac_dir, env=env).returncode == 0 def _find_include(libdir, incpaths): @@ -84,7 +89,7 @@ def build_ginac_interface(parallel=None, args=None): from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.cmake_builder import handleReadonly - sys.stdout.write("\n**** Building GiNaC interface ****") + sys.stdout.write("\n**** Building GiNaC interface ****\n") if args is None: args = [] @@ -98,7 +103,7 @@ def build_ginac_interface(parallel=None, args=None): raise RuntimeError( 'could not find the GiNaC library; please make sure either to install ' 'the library and development headers system-wide, or include the ' - 'path tt the library in the LD_LIBRARY_PATH environment variable' + 'path to the library in the LD_LIBRARY_PATH environment variable' ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_include_dir = _find_include(ginac_lib_dir, ('ginac', 'ginac.h')) @@ -110,7 +115,7 @@ def build_ginac_interface(parallel=None, args=None): raise RuntimeError( 'could not find the CLN library; please make sure either to install ' 'the library and development headers system-wide, or include the ' - 'path tt the library in the LD_LIBRARY_PATH environment variable' + 'path to the library in the LD_LIBRARY_PATH environment variable' ) cln_lib_dir = os.path.dirname(cln_lib) cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) @@ -136,7 +141,7 @@ def run(self): tmpdir = os.path.join(this_file_dir(), 'ginac') else: tmpdir = os.path.abspath(tempfile.mkdtemp()) - sys.stdout.write("Building in '%s'" % tmpdir) + sys.stdout.write("Building in '%s'\n" % tmpdir) os.chdir(tmpdir) super(ginacBuildExt, self).run() if not self.inplace: @@ -174,21 +179,25 @@ def skip(self): if __name__ == '__main__': - logging.getLogger('pyomo').setLevel(logging.DEBUG) - parallel = None - for i, arg in enumerate(sys.argv): - if arg == '-j': - parallel = int(sys.argv.pop(i + 1)) - sys.argv.pop(i) - break - if arg.startswith('-j'): - if '=' in arg: - parallel = int(arg.split('=')[1]) - else: - parallel = int(arg[2:]) - sys.argv.pop(i) - break - if '--build-deps' in sys.argv: - sys.argv.remove('--build-deps') - build_ginac_library(parallel, []) - build_ginac_interface(parallel, sys.argv[1:]) + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-j", + dest='parallel', + type=int, + default=None, + help="Enable parallel build with PARALLEL cores", + ) + parser.add_argument( + "--build-deps", + dest='build_deps', + action='store_true', + default=False, + help="Download and build the CLN/GiNaC libraries", + ) + options, argv = parser.parse_known_args(sys.argv) + logging.getLogger('pyomo').setLevel(logging.INFO) + if options.build_deps: + build_ginac_library(options.parallel, []) + build_ginac_interface(options.parallel, argv[1:]) From c2c63bc52acedace36de75d6ce09c0d062d1ef84 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 10:09:09 -0600 Subject: [PATCH 1038/1178] Run sympy tests any time sympy is installed --- pyomo/contrib/simplification/tests/test_simplification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index be61631e9f3..27208d42229 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -100,12 +100,12 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) -@unittest.skipIf((not sympy_available) or (ginac_available), 'sympy is not available') +@unittest.skipUnless(sympy_available, 'sympy is not available') class TestSimplificationSympy(TestCase, SimplificationMixin): pass -@unittest.skipIf(not ginac_available, 'GiNaC is not available') +@unittest.skipUnless(ginac_available, 'GiNaC is not available') class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() From b53bbfc67a5e2cdea65008de2b4651670a7df66a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:00:31 -0600 Subject: [PATCH 1039/1178] Define NamedIntEnum --- pyomo/common/enums.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 4d969bf7a9e..121155d4ae8 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -17,6 +17,7 @@ .. autosummary:: ExtendedEnumType + NamedIntEnum Standard Enums: @@ -130,7 +131,21 @@ def __new__(metacls, cls, bases, classdict, **kwds): return super().__new__(metacls, cls, bases, classdict, **kwds) -class ObjectiveSense(enum.IntEnum): +class NamedIntEnum(enum.IntEnum): + """An extended version of :py:class:`enum.IntEnum` that supports + creating members by name as well as value. + + """ + + @classmethod + def _missing_(cls, value): + for member in cls: + if member.name == value: + return member + return None + + +class ObjectiveSense(NamedIntEnum): """Flag indicating if an objective is minimizing (1) or maximizing (-1). While the numeric values are arbitrary, there are parts of Pyomo @@ -150,13 +165,6 @@ class ObjectiveSense(enum.IntEnum): def __str__(self): return self.name - @classmethod - def _missing_(cls, value): - for member in cls: - if member.name == value: - return member - return None - minimize = ObjectiveSense.minimize maximize = ObjectiveSense.maximize From e7540f237b774a7297afed8fa62d6848aae2480a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:02:12 -0600 Subject: [PATCH 1040/1178] Rework Simplifier so we can force the backend mode --- pyomo/contrib/simplification/simplify.py | 66 +++++++++++-------- .../tests/test_simplification.py | 43 +++++------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 94f0ceaa33f..840f3a1c1da 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -9,26 +9,25 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging +import warnings + +from pyomo.common.enums import NamedIntEnum from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import value, is_constant -import logging -import warnings from pyomo.contrib.simplification.ginac import ( interface as ginac_interface, interface_available as ginac_available, ) -logger = logging.getLogger(__name__) - def simplify_with_sympy(expr: NumericExpression): if is_constant(expr): return value(expr) - om, se = sympyify_expression(expr) - se = se.simplify() - new_expr = sympy2pyomo_expression(se, om) + object_map, sympy_expr = sympyify_expression(expr) + new_expr = sympy2pyomo_expression(sympy_expr.simplify(), object_map) if is_constant(new_expr): new_expr = value(new_expr) return new_expr @@ -37,29 +36,40 @@ def simplify_with_sympy(expr: NumericExpression): def simplify_with_ginac(expr: NumericExpression, ginac_interface): if is_constant(expr): return value(expr) - gi = ginac_interface - ginac_expr = gi.to_ginac(expr) - ginac_expr = ginac_expr.normal() - new_expr = gi.from_ginac(ginac_expr) - return new_expr + ginac_expr = ginac_interface.to_ginac(expr) + return ginac_interface.from_ginac(ginac_expr.normal()) class Simplifier(object): - def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: - if ginac_available: - self.gi = ginac_interface.GinacInterface(False) - self.suppress_no_ginac_warnings = suppress_no_ginac_warnings + class Mode(NamedIntEnum): + auto = 0 + sympy = 1 + ginac = 2 + + def __init__( + self, suppress_no_ginac_warnings: bool = False, mode: Mode = Mode.auto + ) -> None: + if mode == Simplifier.Mode.auto: + if ginac_available: + mode = Simplifier.Mode.ginac + else: + if not suppress_no_ginac_warnings: + msg = ( + "GiNaC does not seem to be available. Using SymPy. " + + "Note that the GiNaC interface is significantly faster." + ) + logging.getLogger(__name__).warning(msg) + warnings.warn(msg) + mode = Simplifier.Mode.sympy - def simplify(self, expr: NumericExpression): - if ginac_available: - return simplify_with_ginac(expr, self.gi) + if mode == Simplifier.Mode.ginac: + self.gi = ginac_interface.GinacInterface(False) + self.simplify = self._simplify_with_ginac else: - if not self.suppress_no_ginac_warnings: - msg = ( - "GiNaC does not seem to be available. Using SymPy. " - + "Note that the GiNac interface is significantly faster." - ) - logger.warning(msg) - warnings.warn(msg) - self.suppress_no_ginac_warnings = True - return simplify_with_sympy(expr) + self.simplify = self._simplify_with_sympy + + def _simplify_with_ginac(self, expr: NumericExpression): + return simplify_with_ginac(expr, self.gi) + + def _simplify_with_sympy(self, expr: NumericExpression): + return simplify_with_sympy(expr) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 27208d42229..efa9f903adc 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -9,17 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.unittest import TestCase from pyomo.common import unittest +from pyomo.common.fileutils import this_file_dir from pyomo.contrib.simplification import Simplifier from pyomo.contrib.simplification.simplify import ginac_available from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions -import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -from pyomo.common.dependencies import attempt_import - +from pyomo.core.expr.sympy_tools import sympy_available -sympy, sympy_available = attempt_import('sympy') +import pyomo.environ as pe class SimplificationMixin: @@ -37,8 +35,7 @@ def test_simplify(self): e = x * pe.log(x) der1 = reverse_sd(e)[x] der2 = reverse_sd(der1)[x] - simp = Simplifier() - der2_simp = simp.simplify(der2) + der2_simp = self.simp.simplify(der2) expected = x**-1.0 assertExpressionsEqual(self, expected, der2_simp) @@ -46,8 +43,7 @@ def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = 2 * x - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) expected = 2.0 * x assertExpressionsEqual(self, expected, e2) @@ -55,16 +51,14 @@ def test_sum(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = 2 + x - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) self.compare_against_possible_results(e2, [2.0 + x, x + 2.0]) def test_neg(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = -pe.log(x) - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) self.compare_against_possible_results( e2, [(-1.0) * pe.log(x), pe.log(x) * (-1.0), -pe.log(x)] ) @@ -73,8 +67,7 @@ def test_pow(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = x**2.0 - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) assertExpressionsEqual(self, e, e2) def test_div(self): @@ -82,9 +75,7 @@ def test_div(self): x = m.x = pe.Var() y = m.y = pe.Var() e = x / y + y / x - x / y - simp = Simplifier() - e2 = simp.simplify(e) - print(e2) + e2 = self.simp.simplify(e) self.compare_against_possible_results( e2, [y / x, y * (1.0 / x), y * x**-1.0, x**-1.0 * y] ) @@ -95,25 +86,27 @@ def test_unary(self): func_list = [pe.log, pe.sin, pe.cos, pe.tan, pe.asin, pe.acos, pe.atan] for func in func_list: e = func(x) - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) assertExpressionsEqual(self, e, e2) @unittest.skipUnless(sympy_available, 'sympy is not available') -class TestSimplificationSympy(TestCase, SimplificationMixin): - pass +class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.sympy) @unittest.skipUnless(ginac_available, 'GiNaC is not available') -class TestSimplificationGiNaC(TestCase, SimplificationMixin): +class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.ginac) + def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() p = m.p = pe.Param(mutable=True) e1 = p * x**2 + p * x + p * x**2 - simp = Simplifier() - e2 = simp.simplify(e1) + e2 = self.simp.simplify(e1) self.compare_against_possible_results( e2, [ From 9c220b05e34bfff25e3fae4172b187ed169183c1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:56:34 -0600 Subject: [PATCH 1041/1178] Improve robustness of tar filter --- pyomo/common/download.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 95713e9ef76..8361798817f 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -408,25 +408,25 @@ def filter_fcn(info): f = info.name if os.path.isabs(f) or '..' in f or f.startswith(('/', os.sep)): logger.error( - "malformed (potentially insecure) filename (%s) " - "found in tar archive. Skipping file." % (f,) - ) - return False - target = os.path.realpath(os.path.join(dest, f)) - if os.path.commonpath([target, dest]) != dest: - logger.error( - "malformed (potentially insecure) filename (%s) " - "found in zip archive. Skipping file." % (f,) + "malformed or potentially insecure filename (%s). " + "Skipping file." % (f,) ) return False target = self._splitpath(f) if len(target) <= dirOffset: if not info.isdir(): logger.warning( - "Skipping file (%s) in zip archive due to dirOffset" % (f,) + "Skipping file (%s) in tar archive due to dirOffset." % (f,) ) return False - info.name = '/'.join(target[dirOffset:]) + info.name = f = '/'.join(target[dirOffset:]) + target = os.path.realpath(os.path.join(dest, f)) + if os.path.commonpath([target, dest]) != dest: + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) + return False # Strip high bits & group/other write bits info.mode &= 0o755 return True From e597bb5c390432555990394ab8fa474bfee8dbaa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:57:07 -0600 Subject: [PATCH 1042/1178] Ensure tar file is closed --- pyomo/common/download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 8361798817f..2a91553c728 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -398,8 +398,6 @@ def get_tar_archive(self, url, dirOffset=0): raise RuntimeError( "Target directory (%s) exists, but is not a directory" % (self._fname,) ) - tar_file = tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) - dest = os.path.realpath(self._fname) def filter_fcn(info): # this mocks up the `tarfile` filter introduced in Python @@ -431,7 +429,9 @@ def filter_fcn(info): info.mode &= 0o755 return True - tar_file.extractall(dest, filter(filter_fcn, tar_file.getmembers())) + with tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) as TAR: + dest = os.path.realpath(self._fname) + TAR.extractall(dest, filter(filter_fcn, TAR.getmembers())) def get_gzipped_binary_file(self, url): if self._fname is None: From 70166ffbeb3d39d5ed59cba6f479d041d3da0eb1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:58:19 -0600 Subject: [PATCH 1043/1178] test get_tar_archive() --- pyomo/common/tests/test_download.py | 70 ++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 87108be1c59..8fee0ba7e31 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -9,12 +9,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import io import os import platform import re import shutil -import tempfile import subprocess +import tarfile +import tempfile import pyomo.common.unittest as unittest import pyomo.common.envvar as envvar @@ -22,6 +24,7 @@ from pyomo.common import DeveloperError from pyomo.common.fileutils import this_file from pyomo.common.download import FileDownloader, distro_available +from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output @@ -242,7 +245,7 @@ def test_get_files_requires_set_destination(self): ): f.get_gzipped_binary_file('bogus') - def test_get_test_binary_file(self): + def test_get_text_binary_file(self): tmpdir = tempfile.mkdtemp() try: f = FileDownloader() @@ -263,3 +266,66 @@ def test_get_test_binary_file(self): self.assertEqual(os.path.getsize(target), len(os.linesep)) finally: shutil.rmtree(tmpdir) + + def test_get_tar_archive(self): + tmpdir = tempfile.mkdtemp() + try: + f = FileDownloader() + + # Mock retrieve_url so network connections are not necessary + buf = io.BytesIO() + with tarfile.open(mode="w:gz", fileobj=buf) as TAR: + info = tarfile.TarInfo('b/lnk') + info.size = 0 + info.type = tarfile.SYMTYPE + info.linkname = envvar.PYOMO_CONFIG_DIR + TAR.addfile(info) + for fname in ('a', 'b/c', 'b/d', '/root', 'b/lnk/test'): + info = tarfile.TarInfo(fname) + info.size = 0 + info.type = tarfile.REGTYPE + info.mode = 0o644 + info.mtime = info.uid = info.gid = 0 + info.uname = info.gname = 'root' + TAR.addfile(info) + f.retrieve_url = lambda url: buf.getvalue() + + with self.assertRaisesRegex( + DeveloperError, + r"(?s)target file name has not been initialized " + r"with set_destination_filename".replace(' ', r'\s+'), + ): + f.get_tar_archive(None, 1) + + _tmp = os.path.join(tmpdir, 'a_file') + with open(_tmp, 'w'): + pass + f.set_destination_filename(_tmp) + with self.assertRaisesRegex( + RuntimeError, + r"Target directory \(.*a_file\) exists, but is not a directory", + ): + f.get_tar_archive(None, 1) + + f.set_destination_filename(tmpdir) + with LoggingIntercept() as LOG: + f.get_tar_archive(None, 1) + + self.assertEqual( + LOG.getvalue().strip(), + """ +Skipping file (a) in tar archive due to dirOffset. +malformed or potentially insecure filename (/root). Skipping file. +potentially insecure filename (lnk/test) resolves outside target directory. Skipping file. +""".strip(), + ) + for f in ('c', 'd'): + fname = os.path.join(tmpdir, f) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.isfile(fname)) + for f in ('lnk',): + fname = os.path.join(tmpdir, f) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.islink(fname)) + finally: + shutil.rmtree(tmpdir) From e77be8c59c80fd6028c2bb0d2a456c485501b88b Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Sun, 5 May 2024 17:36:00 -0700 Subject: [PATCH 1044/1178] Code for infeasibility diagnostics called mis (#3172) * getting started moving mis code into Pyomo contrib * we have a test for mis, but it needs more coverage * now testing some exceptions * slight change to doc * black * fixing _get_constraint test * removing some spelling errors * more spelling errors removed * update typos.toml for mis * I forgot to push the __init__.py file in tests * a little documentation cleanup * moved mis to be part of iis * correct bad import in mis test * I didn't realize it would run every py file in the test directory * trying to get the Windows tests to pass by explicitly releasing the logger file handle * run black on test_mis.py * trying to manage the temp dir using the tempfilemanager as a context * catch the error that kills windows tests * run black again * windows started passing, but linux failing; one quick check to see if logging.info helps: * run black again * On windows we are just going to have to leave a log file from the test * add a test for a feasible model * Update pyomo/contrib/iis/mis.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> * Changes suggested by Miranda * run black again * simplifying the code * take care of Miranda's helpful comments * add sorely needed f to format error messages * added suggestions from R. Parker to the comments --------- Co-authored-by: Bernard Knueven Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- .github/workflows/typos.toml | 3 + doc/OnlineDocs/conf.py | 1 + doc/OnlineDocs/contributed_packages/iis.rst | 129 +++++++ pyomo/contrib/iis/__init__.py | 1 + pyomo/contrib/iis/mis.py | 377 ++++++++++++++++++++ pyomo/contrib/iis/tests/test_mis.py | 125 +++++++ pyomo/contrib/iis/tests/trivial_mis.py | 24 ++ 7 files changed, 660 insertions(+) create mode 100644 pyomo/contrib/iis/mis.py create mode 100644 pyomo/contrib/iis/tests/test_mis.py create mode 100644 pyomo/contrib/iis/tests/trivial_mis.py diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 4d69cde34e1..7a38164898b 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,6 +40,9 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" +# contrib package named mis and the acronym whence the name comes +mis = "mis" +MIS = "MIS" # Ignore the shorthand ans for answer ans = "ans" # Ignore the keyword arange diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 1aab4cd76c2..a06ccfbc9bd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -84,6 +84,7 @@ 'sphinx.ext.todo', 'sphinx_copybutton', 'enum_tools.autoenum', + 'sphinx.ext.autosectionlabel', #'sphinx.ext.githubpages', ] diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/OnlineDocs/contributed_packages/iis.rst index 98cb9e30771..fa97c2f8c61 100644 --- a/doc/OnlineDocs/contributed_packages/iis.rst +++ b/doc/OnlineDocs/contributed_packages/iis.rst @@ -1,6 +1,135 @@ +Infeasibility Diagnostics +!!!!!!!!!!!!!!!!!!!!!!!!! + +There are two closely related tools for infeasibility diagnosis: + + - :ref:`Infeasible Irreducible System (IIS) Tool` + - :ref:`Minimal Intractable System finder (MIS) Tool` + +The first simply provides a conduit for solvers that compute an +infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The +second provides similar functionality, but uses the ``mis`` package +contributed to Pyomo. + + Infeasible Irreducible System (IIS) Tool ======================================== .. automodule:: pyomo.contrib.iis.iis .. autofunction:: pyomo.contrib.iis.write_iis + +Minimal Intractable System finder (MIS) Tool +============================================ + +The file ``mis.py`` finds sets of actions that each, independently, +would result in feasibility. The zero-tolerance is whatever the +solver uses, so users may want to post-process output if it is going +to be used for analysis. It also computes a minimal intractable system +(which is not guaranteed to be unique). It was written by Ben Knueven +as part of the watertap project (https://github.com/watertap-org/watertap) +and is therefore governed by a license shown +at the top of ``mis.py``. + +The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf + +Solver +------ + +At the time of this writing, you need to use IPopt even for LPs. + +Quick Start +----------- + +The file ``trivial_mis.py`` is a tiny example listed at the bottom of +this help file, which references a Pyomo model with the Python variable +`m` and has these lines: + +.. code-block:: python + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) + +.. Note:: + This is done instead of solving the problem. + +.. Note:: + IDAES users can pass ``get_solver()`` imported from ``ideas.core.solvers`` + as the solver. + +Interpreting the Output +----------------------- + +Assuming the dependencies are installed, running ``trivial_mis.py`` +(shown below) will +produce a lot of warnings from IPopt and then meaningful output (using a logger). + +Repair Options +^^^^^^^^^^^^^^ + +This output for the trivial example shows three independent ways that the model could be rendered feasible: + + +.. code-block:: text + + Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: + ub of var x[1] by 4.464126126706818e-05 + lb of var x[2] by 0.9999553410114216 + Another feasible solution was found with only the following variable bounds relaxed: + lb of var x[1] by 0.7071067726864677 + ub of var x[2] by 0.41421355687130673 + ub of var y by 0.7071067651855212 + Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: + constraint: c by 0.9999999861866736 + + +Minimal Intractable System (MIS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This output shows a minimal intractable system: + + +.. code-block:: text + + Computed Minimal Intractable System (MIS)! + Constraints / bounds in MIS: + lb of var x[2] + lb of var x[1] + constraint: c + +Constraints / bounds in guards for stability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This part of the report is for nonlinear programs (NLPs). + +When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver +to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` +object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the +set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, +just that “bad things” happen when they’re not included. + +Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the +MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the +constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the +subproblem was not feasible. + +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when its assumptions are not satisfied. + +trivial_mis.py +-------------- + +.. code-block:: python + + import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1,2], bounds=(0,1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) diff --git a/pyomo/contrib/iis/__init__.py b/pyomo/contrib/iis/__init__.py index e8d6a7ac2c3..961ac576d42 100644 --- a/pyomo/contrib/iis/__init__.py +++ b/pyomo/contrib/iis/__init__.py @@ -10,3 +10,4 @@ # ___________________________________________________________________________ from pyomo.contrib.iis.iis import write_iis +from pyomo.contrib.iis.mis import compute_infeasibility_explanation diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py new file mode 100644 index 00000000000..6b6cca8e29c --- /dev/null +++ b/pyomo/contrib/iis/mis.py @@ -0,0 +1,377 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + Neither the name of the University of California, Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, National Energy Technology Laboratory, U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, without imposing a separate written license agreement for such Enhancements, then you hereby grant the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare derivative works, incorporate into other computer software, distribute, and sublicense such enhancements or derivative works thereof, in binary and source code form. +""" +""" +Minimal Intractable System (MIS) finder +Originally written by Ben Knueven as part of the WaterTAP project: + https://github.com/watertap-org/watertap +That's why this file has the watertap copyright notice. + +copied by DLW 18Feb2024 and edited + +See: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf +""" + +import logging +import pyomo.environ as pyo + +from pyomo.core.plugins.transform.add_slack_vars import AddSlackVariables + +from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation + +from pyomo.common.modeling import unique_component_name +from pyomo.common.collections import ComponentMap, ComponentSet + +from pyomo.opt import WriterFactory + +logger = logging.getLogger("pyomo.contrib.iis") +logger.setLevel(logging.INFO) + + +class _VariableBoundsAsConstraints(IsomorphicTransformation): + """Replace all variables bounds and domain information with constraints. + + Leaves fixed Vars untouched (for now) + """ + + def _apply_to(self, instance, **kwds): + + bound_constr_block_name = unique_component_name(instance, "_variable_bounds") + instance.add_component(bound_constr_block_name, pyo.Block()) + bound_constr_block = instance.component(bound_constr_block_name) + + for v in instance.component_data_objects(pyo.Var, descend_into=True): + if v.fixed: + continue + lb, ub = v.bounds + if lb is None and ub is None: + continue + var_name = v.getname(fully_qualified=True) + if lb is not None: + con_name = "lb_for_" + var_name + con = pyo.Constraint(expr=(lb, v, None)) + bound_constr_block.add_component(con_name, con) + if ub is not None: + con_name = "ub_for_" + var_name + con = pyo.Constraint(expr=(None, v, ub)) + bound_constr_block.add_component(con_name, con) + + # now we deactivate the variable bounds / domain + v.domain = pyo.Reals + v.setlb(None) + v.setub(None) + + +def compute_infeasibility_explanation( + model, solver, tee=False, tolerance=1e-8, logger=logger +): + """ + This function attempts to determine why a given model is infeasible. It deploys + two main algorithms: + + 1. Successfully relaxes the constraints of the problem, and reports to the user + some sets of constraints and variable bounds, which when relaxed, creates a + feasible model. + 2. Uses the information collected from (1) to attempt to compute a Minimal + Infeasible System (MIS), which is a set of constraints and variable bounds + which appear to be in conflict with each other. It is minimal in the sense + that removing any single constraint or variable bound would result in a + feasible subsystem. + + Args + ---- + model: A pyomo block + solver: A pyomo solver object or a string for SolverFactory + tee (optional): Display intermediate solves conducted (False) + tolerance (optional): The feasibility tolerance to use when declaring a + constraint feasible (1e-08) + logger:logging.Logger + A logger for messages. Uses pyomo.contrib.mis logger by default. + + """ + # Suggested enhancement: It might be useful to return sets of names for each set of relaxed components, as well as the final minimal infeasible system + + # hold the original harmless + modified_model = model.clone() + + if solver is None: + raise ValueError("A solver must be supplied") + elif isinstance(solver, str): + solver = pyo.SolverFactory(solver) + else: + # assume we have a solver + assert solver.available() + + # first, cache the values we get + _value_cache = ComponentMap() + for v in model.component_data_objects(pyo.Var, descend_into=True): + _value_cache[v] = v.value + + # finding proper reference + if model.parent_block() is None: + common_name = "" + else: + common_name = model.name + "." + + _modified_model_var_to_original_model_var = ComponentMap() + _modified_model_value_cache = ComponentMap() + + for v in model.component_data_objects(pyo.Var, descend_into=True): + modified_model_var = modified_model.find_component(v.name[len(common_name) :]) + + _modified_model_var_to_original_model_var[modified_model_var] = v + _modified_model_value_cache[modified_model_var] = _value_cache[v] + modified_model_var.set_value(_value_cache[v], skip_validation=True) + + # TODO: For WT / IDAES models, we should probably be more + # selective in *what* we elasticize. E.g., it probably + # does not make sense to elasticize property calculations + # and maybe certain other equality constraints calculating + # values. Maybe we shouldn't elasticize *any* equality + # constraints. + # For example, elasticizing the calculation of mass fraction + # makes absolutely no sense and will just be noise for the + # modeler to sift through. We could try to sort the constraints + # such that we look for those with linear coefficients `1` on + # some term and leave those be. + # Alternatively, we could apply this tool to a version of the + # model that has as many as possible of these constraints + # "substituted out". + # move the variable bounds to the constraints + _VariableBoundsAsConstraints().apply_to(modified_model) + + AddSlackVariables().apply_to(modified_model) + slack_block = modified_model._core_add_slack_variables + + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + # start with variable bounds -- these are the easiest to interpret + for c in modified_model._variable_bounds.component_data_objects( + pyo.Constraint, descend_into=True + ): + plus = slack_block.component(f"_slack_plus_{c.name}") + minus = slack_block.component(f"_slack_minus_{c.name}") + assert not (plus is None and minus is None) + if plus is not None: + plus.unfix() + if minus is not None: + minus.unfix() + + # TODO: Elasticizing too much at once seems to cause Ipopt trouble. + # After an initial sweep, we should just fix one elastic variable + # and put everything else on a stack of "constraints to elasticize". + # We elasticize one constraint at a time and fix one constraint at a time. + # After fixing an elastic variable, we elasticize a single constraint it + # appears in and put the remaining constraints on the stack. If the resulting problem + # is feasible, we keep going "down the tree". If the resulting problem is + # infeasible or cannot be solved, we elasticize a single constraint from + # the top of the stack. + # The algorithm stops when the stack is empty and the subproblem is infeasible. + # Along the way, any time the current problem is infeasible we can check to + # see if the current set of constraints in the filter is as a collection of + # infeasible constraints -- to terminate early. + # However, while more stable, this is much more computationally intensive. + # So, we leave the implementation simpler for now and consider this as + # a potential extension if this tool sometimes cannot report a good answer. + # Phase 1 -- build the initial set of constraints, or prove feasibility + msg = "" + fixed_slacks = ComponentSet() + elastic_filter = ComponentSet() + + def _constraint_loop(relaxed_things, msg): + if msg == "": + msg += f"Model {model.name} may be infeasible. A feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + while True: + + def _constraint_generator(): + elastic_filter_size_initial = len(elastic_filter) + for v in slack_block.component_data_objects(pyo.Var): + if v.value > tolerance: + constr = _get_constraint(modified_model, v) + yield constr, v.value + v.fix(0) + fixed_slacks.add(v) + elastic_filter.add(constr) + if len(elastic_filter) == elastic_filter_size_initial: + raise Exception(f"Found model {model.name} to be feasible!") + + msg = _get_results_with_value(_constraint_generator(), msg) + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + break + return msg + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("variable bounds", msg) + + # next, try relaxing the inequality constraints + for v in slack_block.component_data_objects(pyo.Var): + c = _get_constraint(modified_model, v) + if c.equality: + # equality constraint + continue + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("inequality constraints and/or variable bounds", msg) + + for v in slack_block.component_data_objects(pyo.Var): + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop( + "inequality constraints, equality constraints, and/or variable bounds", msg + ) + + if len(elastic_filter) == 0: + # load the feasible solution into the original model + for modified_model_var, v in _modified_model_var_to_original_model_var.items(): + v.set_value(modified_model_var.value, skip_validation=True) + results = solver.solve(model, tee=tee) + if pyo.check_optimal_termination(results): + logger.info(f"A feasible solution was found!") + else: + logger.info( + f"Could not find a feasible solution with violated constraints or bounds. This model is likely unstable" + ) + + # Phase 2 -- deletion filter + # remove slacks by fixing them to 0 + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + for o in modified_model.component_data_objects(pyo.Objective, descend_into=True): + o.deactivate() + + # mark all constraints not in the filter as inactive + for c in modified_model.component_data_objects(pyo.Constraint): + if c in elastic_filter: + continue + else: + c.deactivate() + + try: + results = solver.solve(modified_model, tee=tee) + except: + results = None + + if pyo.check_optimal_termination(results): + msg += "Could not determine Minimal Intractable System\n" + else: + deletion_filter = [] + guards = [] + for constr in elastic_filter: + constr.deactivate() + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + math_failure = False + try: + results = solver.solve(modified_model, tee=tee) + except: + math_failure = True + + if math_failure: + constr.activate() + guards.append(constr) + elif pyo.check_optimal_termination(results): + constr.activate() + deletion_filter.append(constr) + else: # still infeasible without this constraint + pass + + msg += "Computed Minimal Intractable System (MIS)!\n" + msg += "Constraints / bounds in MIS:\n" + msg = _get_results(deletion_filter, msg) + msg += "Constraints / bounds in guards for stability:" + msg = _get_results(guards, msg) + + logger.info(msg) + + +def _get_results_with_value(constr_value_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long + if msg is None: + msg = "" + for c, value in constr_value_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]} by {value}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]} by {value}\n" + else: + raise RuntimeError("unrecognized var name") + else: + msg += f"\tconstraint: {c_name} by {value}\n" + return msg + + +def _get_results(constr_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long + if msg is None: + msg = "" + for c in constr_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]}\n" + else: + raise RuntimeError("unrecognized var name") + else: + msg += f"\tconstraint: {c_name}\n" + return msg + + +def _get_constraint(modified_model, v): + if "_slack_plus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_plus_") :]) + if constr is None: + raise RuntimeError( + f"Bad constraint name {v.local_name[len('_slack_plus_'):]}" + ) + return constr + elif "_slack_minus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_minus_") :]) + if constr is None: + raise RuntimeError( + f"Bad constraint name {v.local_name[len('_slack_minus_'):]}" + ) + return constr + else: + raise RuntimeError(f"Bad var name {v.name}") diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py new file mode 100644 index 00000000000..bbdb2367016 --- /dev/null +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -0,0 +1,125 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.environ as pyo +import pyomo.contrib.iis.mis as mis +from pyomo.contrib.iis.mis import _get_constraint +from pyomo.common.tempfiles import TempfileManager + +import logging +import os + + +def _get_infeasible_model(): + m = pyo.ConcreteModel("trivial4test") + m.x = pyo.Var(within=pyo.Binary) + m.y = pyo.Var(within=pyo.NonNegativeReals) + + m.c1 = pyo.Constraint(expr=m.y <= 100.0 * m.x) + m.c2 = pyo.Constraint(expr=m.y <= -100.0 * m.x) + m.c3 = pyo.Constraint(expr=m.x >= 0.5) + + m.o = pyo.Objective(expr=-m.y) + + return m + + +def _get_feasible_model(): + m = pyo.ConcreteModel("Trivial Feasible Quad") + m.x = pyo.Var([1, 2], bounds=(0, 1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] >= -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + return m + + +class TestMIS(unittest.TestCase): + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(exception_flag=False), + "ipopt not available", + ) + def test_write_mis_ipopt(self): + _test_mis("ipopt") + + def test__get_constraint_errors(self): + # A not-completely-cynical way to get the coverage up. + m = _get_infeasible_model() # not modified + fct = _get_constraint + + m.foo_slack_plus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_plus_) + m.foo_slack_minus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_minus_) + m.foo_bar = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_bar) + + def test_feasible_model(self): + m = _get_feasible_model() + opt = pyo.SolverFactory("ipopt") + self.assertRaises(Exception, mis.compute_infeasibility_explanation, m, opt) + + +def _check_output(file_name): + # pretty simple check for now + with open(file_name, "r+") as file1: + lines = file1.readlines() + trigger = "Constraints / bounds in MIS:" + nugget = "lb of var y" + live = False # (long i) + found_nugget = False + for line in lines: + if trigger in line: + live = True + if live: + if nugget in line: + found_nugget = True + if not found_nugget: + raise RuntimeError(f"Did not find '{nugget}' after '{trigger}' in output") + else: + pass + + +def _test_mis(solver_name): + m = _get_infeasible_model() + opt = pyo.SolverFactory(solver_name) + + # This test seems to fail on Windows as it unlinks the tempfile, so live with it + # On a Windows machine, we will not use a temp dir and just try to delete the log file + if os.name == "nt": + file_name = f"_test_mis_{solver_name}.log" + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + # os.remove(file_name) cannot remove it on Windows. Still in use. + + else: # not windows + with TempfileManager.new_context() as tmpmgr: + tmp_path = tmpmgr.mkdtemp() + file_name = os.path.join(tmp_path, f"_test_mis_{solver_name}.log") + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/iis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py new file mode 100644 index 00000000000..4cf0dd7a357 --- /dev/null +++ b/pyomo/contrib/iis/tests/trivial_mis.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +import pyomo.environ as pyo + +m = pyo.ConcreteModel("Trivial Quad") +m.x = pyo.Var([1, 2], bounds=(0, 1)) +m.y = pyo.Var(bounds=(0, 1)) +m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) +m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + +from pyomo.contrib.iis.mis import compute_infeasibility_explanation + +# Note: this particular little problem is quadratic +# As of 18Feb2024 DLW is not sure the explanation code works with solvers other than ipopt +ipopt = pyo.SolverFactory("ipopt") +compute_infeasibility_explanation(m, solver=ipopt) From de1c782ee1ffac58c31ea2f164c40d66abe93b6c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 13:52:35 -0600 Subject: [PATCH 1045/1178] Cleaning up a few docstrings in parmest --- pyomo/contrib/parmest/parmest.py | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 41e1724f94f..70f9de8b84c 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -423,7 +423,8 @@ def _create_parmest_model(self, experiment_number): for obj in model.component_objects(pyo.Objective): obj.deactivate() - # TODO, this needs to be turned a enum class of options that still support custom functions + # TODO, this needs to be turned into an enum class of options that still support + # custom functions if self.obj_function == 'SSE': second_stage_rule = SSE else: @@ -635,7 +636,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): initialize_parmest_model: boolean If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True + parameter estimation, and set flag model_initialized to True. Default is False. Returns ------- @@ -866,10 +867,11 @@ def theta_est( return_values: list, optional List of Variable names, used to return values from the model for data reconciliation calc_cov: boolean, optional - If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver). + Default is False. cov_n: int, optional If calc_cov=True, then the user needs to supply the number of datapoints - that are used in the objective function + that are used in the objective function. Returns ------- @@ -902,9 +904,10 @@ def theta_est( for experiment in self.exp_list ] ) - assert isinstance( - cov_n, int - ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert isinstance(cov_n, int), ( + "The number of datapoints that are used in the objective function is " + "required to calculate the covariance matrix" + ) assert ( cov_n > num_unknowns ), "The number of datapoints must be greater than the number of parameters to estimate" @@ -936,11 +939,12 @@ def theta_est_bootstrap( Size of each bootstrap sample. If samplesize=None, samplesize will be set to the number of samples in the data replacement: bool, optional - Sample with or without replacement + Sample with or without replacement. Default is True. seed: int or None, optional Random seed return_samples: bool, optional - Return a list of sample numbers used in each bootstrap estimation + Return a list of sample numbers used in each bootstrap estimation. + Default is False. Returns ------- @@ -1006,7 +1010,7 @@ def theta_est_leaveNout( seed: int or None, optional Random seed return_samples: bool, optional - Return a list of sample numbers that were left out + Return a list of sample numbers that were left out. Default is False. Returns ------- @@ -1080,7 +1084,7 @@ def leaveNout_bootstrap_test( Random seed Returns - ---------- + ------- List of tuples with one entry per lNo_sample: * The first item in each tuple is the list of N samples that are left @@ -1141,8 +1145,9 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): Values of theta used to compute the objective initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True + If True: Solve square problem instance, build extensive form + of the model for parameter estimation, and set flag + model_initialized to True. Default is False. Returns @@ -1243,7 +1248,7 @@ def likelihood_ratio_test( alphas: list List of alpha values to use in the chi2 test return_thresholds: bool, optional - Return the threshold value for each alpha + Return the threshold value for each alpha. Default is False. Returns ------- @@ -1305,6 +1310,7 @@ def confidence_region_test( to determine if they are inside or outside. Returns + ------- training_results: pd.DataFrame Theta value used to generate the confidence region along with True (inside) or False (outside) for each alpha From 7f27d2f69b0be7027a2907c2de766ede4fab302d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 14:26:55 -0600 Subject: [PATCH 1046/1178] Cleaning up a few assert statements in test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 69155dadb45..65e2e4a3b06 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -59,7 +59,8 @@ def setUp(self): RooneyBieglerExperiment, ) - # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + # Note, the data used in this test has been corrected to use + # data.loc[5,'hour'] = 7 (instead of 6) data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], @@ -109,7 +110,7 @@ def test_bootstrap(self): theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) num_samples = theta_est["samples"].apply(len) - self.assertTrue(len(theta_est.index), 10) + self.assertEqual(len(theta_est.index), 10) self.assertTrue(num_samples.equals(pd.Series([6] * 10))) del theta_est["samples"] @@ -155,13 +156,13 @@ def test_leaveNout(self): results = self.pest.leaveNout_bootstrap_test( 1, None, 3, "Rect", [0.5, 1.0], seed=5436 ) - self.assertTrue(len(results) == 6) # 6 lNo samples + self.assertEqual(len(results), 6) # 6 lNo samples i = 1 samples = results[i][0] # list of N samples that are left out lno_theta = results[i][1] bootstrap_theta = results[i][2] self.assertTrue(samples == [1]) # sample 1 was left out - self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 + self.assertEqual(lno_theta.shape[0], 1) # lno estimate for sample 1 self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) self.assertEqual(lno_theta[1.0].sum(), 1) # all true self.assertEqual(bootstrap_theta.shape[0], 3) # bootstrap for sample 1 @@ -205,7 +206,7 @@ def test_parallel_parmest(self): retcode = ret.returncode else: retcode = subprocess.call(rlist) - assert retcode == 0 + self.assertEqual(retcode, 0) @unittest.skip("Most folks don't have k_aug installed") def test_theta_k_aug_for_Hessian(self): From 5439e9560b1d4b8602350d8de3c9006e1c5fd615 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 15:34:15 -0600 Subject: [PATCH 1047/1178] Fix typo in doe.py --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ab9a5ad9f85..0fc3e8770fe 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1083,7 +1083,7 @@ def _add_objective(self, m): # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) - # If the smallest eigenvalue is (pratcially) negative, add a diagonal matrix to make it positive definite + # If the smallest eigenvalue is (practically) negative, add a diagonal matrix to make it positive definite small_number = 1e-10 if min(eig) < small_number: fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) From 3165c9d67b2b47a8d7ff9e601781ed4e2eecc8b2 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 16:14:10 -0600 Subject: [PATCH 1048/1178] Minor edit to APPSI Highs version method to support older versions of Highs --- pyomo/contrib/appsi/solvers/highs.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3612b9d5014..29c3698b277 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -176,11 +176,23 @@ def available(self): return self.Availability.NotFound def version(self): - version = ( - highspy.HIGHS_VERSION_MAJOR, - highspy.HIGHS_VERSION_MINOR, - highspy.HIGHS_VERSION_PATCH, - ) + try: + version = ( + highspy.HIGHS_VERSION_MAJOR, + highspy.HIGHS_VERSION_MINOR, + highspy.HIGHS_VERSION_PATCH, + ) + except AttributeError: + # Older versions of Highs do not have the above attributes + # and the solver version can only be obtained by making + # an instance of the solver class. + tmp = highspy.Highs() + version = ( + tmp.versionMajor(), + tmp.versionMinor(), + tmp.versionPatch(), + ) + return version @property From b425c4db8e4b6edacf7352b5a0e85de0c69b1558 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 16:21:03 -0600 Subject: [PATCH 1049/1178] Fixing black formatting in highs.py --- pyomo/contrib/appsi/solvers/highs.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 75975a4e0a8..87b9557269f 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -187,11 +187,7 @@ def version(self): # and the solver version can only be obtained by making # an instance of the solver class. tmp = highspy.Highs() - version = ( - tmp.versionMajor(), - tmp.versionMinor(), - tmp.versionPatch(), - ) + version = (tmp.versionMajor(), tmp.versionMinor(), tmp.versionPatch()) return version From c610a20cf594be2dcf34e847aaf63dfb08bfde2b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 16:22:49 -0600 Subject: [PATCH 1050/1178] Removing whitespace in highs.py --- pyomo/contrib/appsi/solvers/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 87b9557269f..c948444839d 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -176,7 +176,7 @@ def available(self): return self.Availability.NotFound def version(self): - try: + try: version = ( highspy.HIGHS_VERSION_MAJOR, highspy.HIGHS_VERSION_MINOR, From fc58199106b62604946d9c20dde95f2b0362e70f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 7 May 2024 06:54:32 -0600 Subject: [PATCH 1051/1178] Clarify comment --- pyomo/contrib/solver/gurobi_direct.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 7b80651ccae..edca7018f92 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -283,7 +283,8 @@ def solve(self, model, **kwds) -> Results: if repn.c.shape[0]: gurobi_model.setAttr('ObjCon', repn.c_offset[0]) gurobi_model.setAttr('ModelSense', int(repn.objectives[0].sense)) - # gurobi_model.update() + # Note: calling gurobi_model.update() here is not + # necessary (it will happen as part of optimize()) timer.stop('transfer_model') options = config.solver_options From 4446d364877ccbaa5faa33f9a100efecbd2975c9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 10:23:43 -0600 Subject: [PATCH 1052/1178] fix tests --- pyomo/environ/tests/test_package_layout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/tests/test_package_layout.py b/pyomo/environ/tests/test_package_layout.py index 4e1574ab158..47c6422a879 100644 --- a/pyomo/environ/tests/test_package_layout.py +++ b/pyomo/environ/tests/test_package_layout.py @@ -38,6 +38,7 @@ _NON_MODULE_DIRS = { join('contrib', 'ampl_function_demo', 'src'), join('contrib', 'appsi', 'cmodel', 'src'), + join('contrib', 'simplification', 'ginac', 'src'), join('contrib', 'pynumero', 'src'), join('core', 'tests', 'data', 'baselines'), join('core', 'tests', 'diet', 'baselines'), From 90b1783197210cdca5dfe3c0b45bb467e6d58148 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 7 May 2024 13:09:01 -0600 Subject: [PATCH 1053/1178] Update tar filter to handle ValueError from commonpath() --- pyomo/common/download.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 2a91553c728..ad672e8c79b 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -419,11 +419,17 @@ def filter_fcn(info): return False info.name = f = '/'.join(target[dirOffset:]) target = os.path.realpath(os.path.join(dest, f)) - if os.path.commonpath([target, dest]) != dest: - logger.error( - "potentially insecure filename (%s) resolves outside target " - "directory. Skipping file." % (f,) - ) + try: + if os.path.commonpath([target, dest]) != dest: + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) + return False + except ValueError: + # commonpath() will raise ValueError for paths that + # don't have anything in common (notably, when files are + # on different drives on Windows) return False # Strip high bits & group/other write bits info.mode &= 0o755 From 5aae45946ebd302cfe0e33d54c5927d2f193a362 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 13:20:22 -0600 Subject: [PATCH 1054/1178] keep mutable parameters in sympy conversion --- pyomo/core/expr/sympy_tools.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 48bd542be0f..05c9885cc8c 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -175,10 +175,11 @@ def sympyVars(self): class Pyomo2SympyVisitor(EXPR.StreamBasedExpressionVisitor): - def __init__(self, object_map): + def __init__(self, object_map, keep_mutable_parameters=True): sympy.Add # this ensures _configure_sympy gets run super(Pyomo2SympyVisitor, self).__init__() self.object_map = object_map + self.keep_mutable_parameters = keep_mutable_parameters def initializeWalker(self, expr): return self.beforeChild(None, expr, None) @@ -212,6 +213,8 @@ def beforeChild(self, node, child, child_idx): # # Everything else is a constant... # + if self.keep_mutable_parameters and child.is_parameter_type() and child.mutable: + return False, self.object_map.getSympySymbol(child) return False, value(child) @@ -245,13 +248,15 @@ def beforeChild(self, node, child, child_idx): return True, None -def sympyify_expression(expr): +def sympyify_expression(expr, keep_mutable_parameters=True): """Convert a Pyomo expression to a Sympy expression""" # # Create the visitor and call it. # object_map = PyomoSympyBimap() - visitor = Pyomo2SympyVisitor(object_map) + visitor = Pyomo2SympyVisitor( + object_map, keep_mutable_parameters=keep_mutable_parameters + ) return object_map, visitor.walk_expression(expr) From ae5ebd3ed492405ae488921ef3c6b36c886f098a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 13:26:35 -0600 Subject: [PATCH 1055/1178] update defaults for mutable parameters when using sympy --- pyomo/contrib/simplification/simplify.py | 2 +- pyomo/core/expr/sympy_tools.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 840f3a1c1da..874b5b1e801 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -26,7 +26,7 @@ def simplify_with_sympy(expr: NumericExpression): if is_constant(expr): return value(expr) - object_map, sympy_expr = sympyify_expression(expr) + object_map, sympy_expr = sympyify_expression(expr, keep_mutable_parameters=True) new_expr = sympy2pyomo_expression(sympy_expr.simplify(), object_map) if is_constant(new_expr): new_expr = value(new_expr) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 05c9885cc8c..6c184f0e4c4 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -175,7 +175,7 @@ def sympyVars(self): class Pyomo2SympyVisitor(EXPR.StreamBasedExpressionVisitor): - def __init__(self, object_map, keep_mutable_parameters=True): + def __init__(self, object_map, keep_mutable_parameters=False): sympy.Add # this ensures _configure_sympy gets run super(Pyomo2SympyVisitor, self).__init__() self.object_map = object_map @@ -248,7 +248,7 @@ def beforeChild(self, node, child, child_idx): return True, None -def sympyify_expression(expr, keep_mutable_parameters=True): +def sympyify_expression(expr, keep_mutable_parameters=False): """Convert a Pyomo expression to a Sympy expression""" # # Create the visitor and call it. From e4920cbd229fc9712bf03e972c7788e9e0cb1eb6 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 7 May 2024 15:46:50 -0400 Subject: [PATCH 1056/1178] add test for call_before_subproblem_solve --- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index 37969276d55..f54e766baa4 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -101,6 +101,30 @@ def test_OA_rNLP(self): ) self.check_optimal_solution(model) + def test_OA_callback(self): + """Test the outer approximation decomposition algorithm.""" + with SolverFactory('mindtpy') as opt: + + def callback(model): + model.Y[1].value = 0 + model.Y[2].value = 0 + model.Y[3].value = 0 + + model = SimpleMINLP2() + # The callback function will make the OA method cycling. + results = opt.solve( + model, + strategy='OA', + init_strategy='rNLP', + mip_solver=required_solvers[1], + nlp_solver=required_solvers[0], + call_before_subproblem_solve=callback, + ) + self.assertIs( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertAlmostEqual(value(results.problem.lower_bound), 5, places=1) + def test_OA_extreme_model(self): """Test the outer approximation decomposition algorithm.""" with SolverFactory('mindtpy') as opt: From 2b3bd4eea0d00e4e7e2dce3483aceaa04dcfb767 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 14:38:14 -0600 Subject: [PATCH 1057/1178] update tests --- .../tests/test_simplification.py | 25 ++++++++++--------- setup.py | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index efa9f903adc..acef0af502e 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -89,18 +89,6 @@ def test_unary(self): e2 = self.simp.simplify(e) assertExpressionsEqual(self, e, e2) - -@unittest.skipUnless(sympy_available, 'sympy is not available') -class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): - def setUp(self): - self.simp = Simplifier(mode=Simplifier.Mode.sympy) - - -@unittest.skipUnless(ginac_available, 'GiNaC is not available') -class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): - def setUp(self): - self.simp = Simplifier(mode=Simplifier.Mode.ginac) - def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() @@ -116,5 +104,18 @@ def test_param(self): p * x + 2.0 * p * x**2.0, x**2.0 * p * 2.0 + p * x, p * x + x**2.0 * p * 2.0, + p * x * (1 + 2 * x), ], ) + + +@unittest.skipUnless(sympy_available, 'sympy is not available') +class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.sympy) + + +@unittest.skipUnless(ginac_available, 'GiNaC is not available') +class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.ginac) diff --git a/setup.py b/setup.py index 70c1626a650..a125b02b2fe 100644 --- a/setup.py +++ b/setup.py @@ -306,6 +306,7 @@ def __ne__(self, other): "pyomo.contrib.mcpp": ["*.cpp"], "pyomo.contrib.pynumero": ['src/*', 'src/tests/*'], "pyomo.contrib.viewer": ["*.ui"], + "pyomo.contrib.simplification.ginac": ["src/*.cpp", "src/*.hpp"], }, ext_modules=ext_modules, entry_points=""" From 7c0741a1cebff1e5b17228543e55f77c6f1413de Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 07:38:05 -0600 Subject: [PATCH 1058/1178] Make _SequenceVarData public to match recent change in pyomo/main --- pyomo/contrib/cp/repn/docplex_writer.py | 4 ++-- pyomo/contrib/cp/sequence_var.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 53d14495c4b..6a0eb7749a8 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -33,7 +33,7 @@ from pyomo.contrib.cp.sequence_var import ( SequenceVar, ScalarSequenceVar, - _SequenceVarData, + SequenceVarData, ) from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( AlternativeExpression, @@ -1055,7 +1055,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, ScalarSequenceVar: _before_sequence_var, - _SequenceVarData: _before_sequence_var, + SequenceVarData: _before_sequence_var, ScalarVar: _before_var, VarData: _before_var, IndexedVar: _before_indexed_var, diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 486776f58da..cb42f445dc3 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) -class _SequenceVarData(ActiveComponentData): +class SequenceVarData(ActiveComponentData): """This class defines the abstract interface for a single sequence variable.""" __slots__ = ('interval_vars',) @@ -62,7 +62,7 @@ def set_value(self, expr): @ModelComponentFactory.register("Sequences of IntervalVars") class SequenceVar(ActiveIndexedComponent): - _ComponentDataClass = _SequenceVarData + _ComponentDataClass = SequenceVarData def __new__(cls, *args, **kwds): if cls != SequenceVar: @@ -100,7 +100,7 @@ def _getitem_when_not_present(self, index): def construct(self, data=None): """ - Construct the _SequenceVarData objects for this SequenceVar + Construct the SequenceVarData objects for this SequenceVar """ if self._constructed: return @@ -140,9 +140,9 @@ def _pprint(self): ) -class ScalarSequenceVar(_SequenceVarData, SequenceVar): +class ScalarSequenceVar(SequenceVarData, SequenceVar): def __init__(self, *args, **kwds): - _SequenceVarData.__init__(self, component=self) + SequenceVarData.__init__(self, component=self) SequenceVar.__init__(self, *args, **kwds) self._index = UnindexedComponent_index From f76108584d5be2f12259055cfac0529a39c7e8c1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 08:19:01 -0600 Subject: [PATCH 1059/1178] Create basic autodoc for MAiNGO --- .../appsi/appsi.solvers.maingo.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst new file mode 100644 index 00000000000..21e61c38d51 --- /dev/null +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst @@ -0,0 +1,14 @@ +MAiNGO +====== + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO + :members: + :inherited-members: + :undoc-members: + :show-inheritance: From fbd9a0ab2d8a73babc3d67acfb6cc71d4d92677e Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 08:22:32 -0600 Subject: [PATCH 1060/1178] Update APPSI TOC --- doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst index 1c598d95628..f4dcb81b4be 100644 --- a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst @@ -13,3 +13,4 @@ Solvers appsi.solvers.cplex appsi.solvers.cbc appsi.solvers.highs + appsi.solvers.maingo From 82dfda15e1ce4650ff3d7b7a2abfd2c90735a8bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 08:28:12 -0600 Subject: [PATCH 1061/1178] Ensure the same output is logged on Windows and other platforms --- pyomo/common/download.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index ad672e8c79b..ad3b64060e9 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -430,6 +430,10 @@ def filter_fcn(info): # commonpath() will raise ValueError for paths that # don't have anything in common (notably, when files are # on different drives on Windows) + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) return False # Strip high bits & group/other write bits info.mode &= 0o755 From 8ebd61ccd5c8a3e8f5e62418024ea73130961c68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 09:54:39 -0600 Subject: [PATCH 1062/1178] Generate binary vectors without going through strings --- .../piecewise/transform/disaggregated_logarithmic.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py index d5de010f308..d582cdcfff5 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -193,6 +193,10 @@ def x_constraint(b, i): # TODO test the Gray codes too # note: Must have num != 0 and ceil(log2(num)) > length to be valid def _get_binary_vector(self, num, length): - # Use python's string formatting instead of bothering with modular - # arithmetic. Hopefully not slow. - return tuple(int(x) for x in format(num, f"0{length}b")) + ans = [] + for i in range(length): + ans.append(num & 1) + num >>= 1 + assert not num + ans.reverse() + return tuple(ans) From 3572c445145b8e901d1de61cc842914c5ea60d8a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 May 2024 10:37:39 -0600 Subject: [PATCH 1063/1178] ginac cleanup --- pyomo/contrib/simplification/build.py | 1 + pyomo/contrib/simplification/ginac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d540991b010..dfb9d2cf1c8 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -71,6 +71,7 @@ def build_ginac_library(parallel=None, argv=None, env=None): logger.info("\nBuilding GiNaC\n") assert subprocess.run(make_cmd, cwd=ginac_dir, env=env).returncode == 0 assert subprocess.run(install_cmd, cwd=ginac_dir, env=env).returncode == 0 + print("Installed GiNaC to %s" % (ginac_dir,)) def _find_include(libdir, incpaths): diff --git a/pyomo/contrib/simplification/ginac/__init__.py b/pyomo/contrib/simplification/ginac/__init__.py index af6511944de..6896bec12c4 100644 --- a/pyomo/contrib/simplification/ginac/__init__.py +++ b/pyomo/contrib/simplification/ginac/__init__.py @@ -30,7 +30,7 @@ def _importer(): # GiNaC needs 2 libraries that are generally dynamically linked # to the interface library. If we built those ourselves, then # the libraries will be PYOMO_CONFIG_DIR/lib ... but that - # directlor is very likely to NOT be on the library search path + # directory is very likely to NOT be on the library search path # when the Python interpreter was started. We will manually # look for those two libraries, and if we find them, load them # into this process (so the interface can find them) From a53c6f7ec4929a5821203ee0a87e8d31e88edb1d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 10:39:54 -0600 Subject: [PATCH 1064/1178] Add 'builders' marker for testing custom library builds (currently just ginac) --- .jenkins.sh | 7 +++++++ pyomo/contrib/simplification/tests/test_simplification.py | 2 ++ setup.cfg | 1 + 3 files changed, 10 insertions(+) diff --git a/.jenkins.sh b/.jenkins.sh index 37be6113ed9..37a9238f983 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -122,6 +122,13 @@ if test -z "$MODE" -o "$MODE" == setup; then echo "PYOMO_CONFIG_DIR=$PYOMO_CONFIG_DIR" echo "" + # Call Pyomo build scripts to build TPLs that would normally be + # skipped by the pyomo download-extensions / build-extensions + # actions below + if test [ " $CATEGORY " == *" builders "*; then + python pyomo/contrib/simplification/build.py --build-deps || exit 1 + fi + # Use Pyomo to download & compile binary extensions i=0 while /bin/true; do diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index acef0af502e..1ff9f5a3cc4 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -115,6 +115,8 @@ def setUp(self): self.simp = Simplifier(mode=Simplifier.Mode.sympy) +@unittest.pytest.mark.default +@unittest.pytest.mark.builders @unittest.skipUnless(ginac_available, 'GiNaC is not available') class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): def setUp(self): diff --git a/setup.cfg b/setup.cfg index b606138f38c..d9ccbbb7c5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + builders: thests that should be run when testing custom (extension) builders \ No newline at end of file From 8f33eed1ed2a82c24be5d5b3dff77dcab472f67e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 10:40:36 -0600 Subject: [PATCH 1065/1178] Support multiple markers (categories) in jenkins driver --- .jenkins.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.jenkins.sh b/.jenkins.sh index 37a9238f983..8c72edf41c0 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -43,8 +43,8 @@ fi if test -z "$SLIM"; then export VENV_SYSTEM_PACKAGES='--system-site-packages' fi -if test ! -z "$CATEGORY"; then - export PY_CAT="-m $CATEGORY" +if test -n "$CATEGORY"; then + export PY_CAT="-m '"`echo "$CATEGORY" | sed -r "s/ +/ or /g"`"'" fi if test "$WORKSPACE" != "`pwd`"; then From 4dc4e893aa861bcbeaf777e971788f5e8ce39983 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 10:41:01 -0600 Subject: [PATCH 1066/1178] Additional (debugging) output in ginac_interface builder --- pyomo/contrib/simplification/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d540991b010..a1332490b1b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -155,6 +155,7 @@ def run(self): ) if not os.path.exists(target): os.makedirs(target) + sys.stdout.write(f"Installing {library} in {target}\n") shutil.copy(library, target) package_config = { From 22ee3c76e9974960e156f567b332ddf6746c6021 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 10:57:24 -0600 Subject: [PATCH 1067/1178] Updating CHANGELOG in preparation for the 6.7.2 release --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06e0f71378..11a9f4a3020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,78 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.2 (9 May 2024) +------------------------------------------------------------------------------- + +- General + - Support config domains with either method or attribute domain_name (#3159) + - Update TPL package list due to contrib.solver (#3164) + - Automate TPL callback registrations (#3167) + - Fix type registrations for ExternalFunction arguments (#3168) + - Only modify module path and spec for deferred import modules (#3176) + - Add "mixed" standard form representation (#3201) + - Support "default" dispatchers in `ExitNodeDispatcher` (#3194) + - Redefine objective sense as a proper `IntEnum` (#3224) + - Fix division-by-0 bug in linear walker (#3246) +- Core + - Allow `Var` objects in `LinearExpression.args` (#3189) + - Add type hints to components (#3173) + - Simplify expressions generated by `TemplateSumExpression` (#3196) + - Make component data public classes (#3221, #3253) + - Exploit repeated named expressions in `identify_variables` (#3190) +- Documentation + - NFC: Add link to the HOMOWP companion notebooks (#3195) + - Update installation documentation to include Cython instructions (#3208) + - Add links to the Pyomo Book Springer page (#3211) +- Solver Interfaces + - Fix division by zero error in linear presolve (#3161) + - Subprocess timeout update (#3183) + - Solver Refactor - Allow no objective (#3181) + - NLv2: handle presolved independent linear subsystems (#3193) + - Update `LegacySolverWrapper` to be compatible with the `pyomo` script (#3202) + - Fix mosek_direct to use putqconk instead of putqcon (#3199) + - Solver Refactor - Bug fixes for IDAES Integration (#3214) + - Check _skip_trivial_constraints before the constraint body (#3226) + - Fix AMPL solver duplicate funcadd (#3206) + - Solver Refactor - Fix bugs in setting `name` and `solutions` attributes (#3228) + - Disable the use of universal newlines in the ipopt_v2 NL file (#3231) + - NLv2: fix reporting numbers of nonlinear discrete variables (#3238) + - Fix: Get SCIP solving time considering float number with some text (#3234) + - Solver Refactor - Add `gurobi_direct` implementation (#3225) +- Testing + - Set maxDiff=None on the base TestCase class (#3171) + - Testing infrastructure updates (#3175) + - Typos update for March 2024 (#3219) + - Add openmpi to testing environment to work around issue in mpi4py (#3236, #3239) + - Skip black 24.4.1 due to a bug in the parser (#3247) + - Skip tests on draft and WIP pull requests (#3223) + - Update GHA to grab gurobipy from PyPI (#3254) +- GDP + - Use private_data for all mappings between original and transformed components (#3166) + - Fix a bug in gdp.bigm transformation for nested GDPs (#3213) +- Contributed Packages + - APPSI: Allow cmodel to handle non-mutable params in var and constraint bounds (#3182) + - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) + - APPSI: Add MAiNGO solver interface (#3165) + - DoE: Bug fixes (#3245) + - incidence_analysis: Improve performance of `solve_strongly_connected_components` for + models with named expressions (#3186) + - incidence_analysis: Add function to plot incidence graph in Dulmage-Mendelsohn order (#3207) + - incidence_analysis: Require variables and constraints to be specified separately in + `IncidenceGraphInterface.remove_nodes` (#3212) + - latex_printer: Resolve errors for set operations / multidimensional sets (#3177) + - MindtPy: Add Highs support (#2971) + - MindtPy: Add call_before_subproblem_solve callback (#3251) + - Parmest: New UI using experiment lists (#3160) + - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) + - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) + - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) + - PyNumero: Include "inventory" in readme (#3248) + - PyROS: Simplify custom domain validators (#3169) + - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) + - PyROS: Update solver timing system (#3198) + ------------------------------------------------------------------------------- Pyomo 6.7.1 (21 Feb 2024) ------------------------------------------------------------------------------- From 3bfa3bd1e8b1610217052fdf48cf4ff63a9b5f1f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 10:57:58 -0600 Subject: [PATCH 1068/1178] Pinning to numpy<2.0.0 for the release --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 70c1626a650..8817649ecc2 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy', # Needed by autodoc for pynumero + 'numpy<2.0.0', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -271,7 +271,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy', + 'numpy<2.0.0', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 1d1f131b940e00a351fae66f8ad9260f9029084c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 11:01:22 -0600 Subject: [PATCH 1069/1178] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a9f4a3020..683551ba03a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Pyomo 6.7.2 (9 May 2024) - APPSI: Allow cmodel to handle non-mutable params in var and constraint bounds (#3182) - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) - APPSI: Add MAiNGO solver interface (#3165) + - CP: Add SequenceVar and other logical expressions for scheduling (#3227) - DoE: Bug fixes (#3245) - incidence_analysis: Improve performance of `solve_strongly_connected_components` for models with named expressions (#3186) From 38b966298ace3b1b06f73501a8e436f4031a51be Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 11:13:38 -0600 Subject: [PATCH 1070/1178] Reorder and merge some items --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 683551ba03a..d61758f7c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ Pyomo 6.7.2 (9 May 2024) - General - Support config domains with either method or attribute domain_name (#3159) - - Update TPL package list due to contrib.solver (#3164) - Automate TPL callback registrations (#3167) - Fix type registrations for ExternalFunction arguments (#3168) - Only modify module path and spec for deferred import modules (#3176) @@ -29,19 +28,18 @@ Pyomo 6.7.2 (9 May 2024) - Solver Interfaces - Fix division by zero error in linear presolve (#3161) - Subprocess timeout update (#3183) - - Solver Refactor - Allow no objective (#3181) + - Solver Refactor - Bug fixes for various components (#3181, #3214, #3228) - NLv2: handle presolved independent linear subsystems (#3193) - Update `LegacySolverWrapper` to be compatible with the `pyomo` script (#3202) - Fix mosek_direct to use putqconk instead of putqcon (#3199) - - Solver Refactor - Bug fixes for IDAES Integration (#3214) - Check _skip_trivial_constraints before the constraint body (#3226) - Fix AMPL solver duplicate funcadd (#3206) - - Solver Refactor - Fix bugs in setting `name` and `solutions` attributes (#3228) - Disable the use of universal newlines in the ipopt_v2 NL file (#3231) - NLv2: fix reporting numbers of nonlinear discrete variables (#3238) - Fix: Get SCIP solving time considering float number with some text (#3234) - Solver Refactor - Add `gurobi_direct` implementation (#3225) - Testing + - Update TPL package list due to `contrib.solver` (#3164) - Set maxDiff=None on the base TestCase class (#3171) - Testing infrastructure updates (#3175) - Typos update for March 2024 (#3219) @@ -64,7 +62,7 @@ Pyomo 6.7.2 (9 May 2024) - incidence_analysis: Require variables and constraints to be specified separately in `IncidenceGraphInterface.remove_nodes` (#3212) - latex_printer: Resolve errors for set operations / multidimensional sets (#3177) - - MindtPy: Add Highs support (#2971) + - MindtPy: Add HiGHS support (#2971) - MindtPy: Add call_before_subproblem_solve callback (#3251) - Parmest: New UI using experiment lists (#3160) - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) From e847f10f032fc2a04080af7784d671847a51adcc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 11:25:24 -0600 Subject: [PATCH 1071/1178] NFC: fix typo --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d9ccbbb7c5e..f670cef8f68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - builders: thests that should be run when testing custom (extension) builders \ No newline at end of file + builders: tests that should be run when testing custom (extension) builders \ No newline at end of file From 8b11a1c789a73219054fec5d25b8db22f42e8da8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 11:27:51 -0600 Subject: [PATCH 1072/1178] NFC: resyncing test_branches and test_pr_and_main --- .github/workflows/test_branches.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index df6455568b9..d9c36e78fc4 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -96,7 +96,7 @@ jobs: PACKAGES: openmpi mpi4py - os: ubuntu-latest - python: 3.11 + python: '3.11' other: /singletest category: "-m 'neos or importtest'" skip_doctest: 1 @@ -273,7 +273,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip gurobipy==10.0.3\ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From 3a33d89ff10707c707fc735285afcd8213868f94 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 16:49:53 -0600 Subject: [PATCH 1073/1178] More edits to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61758f7c1c..954231f9f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Pyomo 6.7.2 (9 May 2024) - MindtPy: Add HiGHS support (#2971) - MindtPy: Add call_before_subproblem_solve callback (#3251) - Parmest: New UI using experiment lists (#3160) + - piecewise: Add piecewise linear transformations (#3036) - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) From fa2cc317eb90941c87997bb7b7935aca488cc1a6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:04:32 -0600 Subject: [PATCH 1074/1178] improve handling of category quotation --- .jenkins.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.jenkins.sh b/.jenkins.sh index 8c72edf41c0..a00b42eac4e 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -43,9 +43,6 @@ fi if test -z "$SLIM"; then export VENV_SYSTEM_PACKAGES='--system-site-packages' fi -if test -n "$CATEGORY"; then - export PY_CAT="-m '"`echo "$CATEGORY" | sed -r "s/ +/ or /g"`"'" -fi if test "$WORKSPACE" != "`pwd`"; then echo "ERROR: pwd is not WORKSPACE" @@ -185,7 +182,7 @@ if test -z "$MODE" -o "$MODE" == test; then python -m pytest -v \ -W ignore::Warning \ --junitxml="TEST-pyomo.xml" \ - $PY_CAT $TEST_SUITES $PYTEST_EXTRA_ARGS + -m "$CATEGORY" $TEST_SUITES $PYTEST_EXTRA_ARGS # Combine the coverage results and upload if test -z "$DISABLE_COVERAGE"; then From bd5f10cb7d0dbd96aae6114e9ee51407e9b4dd13 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:07:16 -0600 Subject: [PATCH 1075/1178] Improve conftest.py efficiency --- conftest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 7faad6fc89b..00abdecfa12 100644 --- a/conftest.py +++ b/conftest.py @@ -11,6 +11,8 @@ import pytest +_implicit_markers = {'default',} +_extended_implicit_markers = _implicit_markers.union({'solver',}) def pytest_runtest_setup(item): """ @@ -32,13 +34,10 @@ def pytest_runtest_setup(item): the default mode; but if solver tests are also marked with an explicit category (e.g., "expensive"), we will skip them. """ - marker = item.iter_markers() solvernames = [mark.args[0] for mark in item.iter_markers(name="solver")] solveroption = item.config.getoption("--solver") markeroption = item.config.getoption("-m") - implicit_markers = ['default'] - extended_implicit_markers = implicit_markers + ['solver'] - item_markers = set(mark.name for mark in marker) + item_markers = set(mark.name for mark in item.iter_markers()) if solveroption: if solveroption not in solvernames: pytest.skip("SKIPPED: Test not marked {!r}".format(solveroption)) @@ -46,9 +45,9 @@ def pytest_runtest_setup(item): elif markeroption: return elif item_markers: - if not set(implicit_markers).issubset( + if not _implicit_markers.issubset( item_markers - ) and not item_markers.issubset(set(extended_implicit_markers)): + ) and not item_markers.issubset(_extended_implicit_markers): pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.') From 354feb2c9a81473bc9ca95138bcd8e4d4eadd85c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:07:46 -0600 Subject: [PATCH 1076/1178] Ensure that all unmarked tests are marked with the implicit markers --- conftest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/conftest.py b/conftest.py index 00abdecfa12..6e8043c2c92 100644 --- a/conftest.py +++ b/conftest.py @@ -14,6 +14,18 @@ _implicit_markers = {'default',} _extended_implicit_markers = _implicit_markers.union({'solver',}) +def pytest_collection_modifyitems(items): + """ + This method will mark any unmarked tests with the implicit marker ('default') + + """ + for item in items: + try: + next(item.iter_markers()) + except StopIteration: + for marker in _implicit_markers: + item.add_marker(getattr(pytest.mark, marker)) + def pytest_runtest_setup(item): """ This method overrides pytest's default behavior for marked tests. From 92edcc5d7b486a767f0033514e13ce6549d8e81d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:08:29 -0600 Subject: [PATCH 1077/1178] NFC: apply black --- conftest.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/conftest.py b/conftest.py index 6e8043c2c92..34b366f9fd6 100644 --- a/conftest.py +++ b/conftest.py @@ -11,8 +11,9 @@ import pytest -_implicit_markers = {'default',} -_extended_implicit_markers = _implicit_markers.union({'solver',}) +_implicit_markers = {'default'} +_extended_implicit_markers = _implicit_markers.union({'solver'}) + def pytest_collection_modifyitems(items): """ @@ -26,6 +27,7 @@ def pytest_collection_modifyitems(items): for marker in _implicit_markers: item.add_marker(getattr(pytest.mark, marker)) + def pytest_runtest_setup(item): """ This method overrides pytest's default behavior for marked tests. @@ -57,9 +59,9 @@ def pytest_runtest_setup(item): elif markeroption: return elif item_markers: - if not _implicit_markers.issubset( - item_markers - ) and not item_markers.issubset(_extended_implicit_markers): + if not _implicit_markers.issubset(item_markers) and not item_markers.issubset( + _extended_implicit_markers + ): pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.') From 7ae72756ad5e0019370ed3aaf9f327b86fd717d1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:47:16 -0600 Subject: [PATCH 1078/1178] Prevent APPSI / GiNaC interfaces from exposing module symbols globally --- pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp | 5 +++-- pyomo/contrib/simplification/ginac/src/ginac_interface.cpp | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp index 6acc1d79845..5a838ffd786 100644 --- a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp +++ b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp @@ -63,7 +63,8 @@ PYBIND11_MODULE(appsi_cmodel, m) { m.def("appsi_exprs_from_pyomo_exprs", &appsi_exprs_from_pyomo_exprs); m.def("appsi_expr_from_pyomo_expr", &appsi_expr_from_pyomo_expr); m.def("prep_for_repn", &prep_for_repn); - py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "PyomoExprTypes", py::module_local()) + .def(py::init<>()); py::class_>(m, "Node") .def("is_variable_type", &Node::is_variable_type) .def("is_param_type", &Node::is_param_type) @@ -165,7 +166,7 @@ PYBIND11_MODULE(appsi_cmodel, m) { .def(py::init<>()) .def("write", &LPWriter::write) .def("get_solve_cons", &LPWriter::get_solve_cons); - py::enum_(m, "ExprType") + py::enum_(m, "ExprType", py::module_local()) .value("py_float", ExprType::py_float) .value("var", ExprType::var) .value("param", ExprType::param) diff --git a/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp index 1060f87161c..9b05baf71ca 100644 --- a/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp @@ -298,7 +298,8 @@ py::object GinacInterface::from_ginac(ex &ge) { PYBIND11_MODULE(ginac_interface, m) { m.def("pyomo_to_ginac", &pyomo_to_ginac); - py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "PyomoExprTypes", py::module_local()) + .def(py::init<>()); py::class_(m, "ginac_expression") .def("expand", [](ex &ge) { return ge.expand(); @@ -313,7 +314,7 @@ PYBIND11_MODULE(ginac_interface, m) { .def(py::init()) .def("to_ginac", &GinacInterface::to_ginac) .def("from_ginac", &GinacInterface::from_ginac); - py::enum_(m, "ExprType") + py::enum_(m, "ExprType", py::module_local()) .value("py_float", ExprType::py_float) .value("var", ExprType::var) .value("param", ExprType::param) From 1409aa2956159b8a062e12ed831db79b57dda189 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:09:15 -0600 Subject: [PATCH 1079/1178] Fix bug in Jenkins driver --- .jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jenkins.sh b/.jenkins.sh index a00b42eac4e..842733e471b 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -122,7 +122,7 @@ if test -z "$MODE" -o "$MODE" == setup; then # Call Pyomo build scripts to build TPLs that would normally be # skipped by the pyomo download-extensions / build-extensions # actions below - if test [ " $CATEGORY " == *" builders "*; then + if test [[ " $CATEGORY " == *" builders "* ]]; then python pyomo/contrib/simplification/build.py --build-deps || exit 1 fi From aa756017fc23091764a63721dc80c94181cbe01a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:29:43 -0600 Subject: [PATCH 1080/1178] Fix typo in Jenkins driver --- .jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jenkins.sh b/.jenkins.sh index 842733e471b..0f4e70d3cf1 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -122,7 +122,7 @@ if test -z "$MODE" -o "$MODE" == setup; then # Call Pyomo build scripts to build TPLs that would normally be # skipped by the pyomo download-extensions / build-extensions # actions below - if test [[ " $CATEGORY " == *" builders "* ]]; then + if [[ " $CATEGORY " == *" builders "* ]]; then python pyomo/contrib/simplification/build.py --build-deps || exit 1 fi From cdaff17f6a7e7c36428df887acdc7a7a59ec836a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:53:56 -0600 Subject: [PATCH 1081/1178] Add info to the build log --- .jenkins.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.jenkins.sh b/.jenkins.sh index 0f4e70d3cf1..696847fd92c 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -123,13 +123,19 @@ if test -z "$MODE" -o "$MODE" == setup; then # skipped by the pyomo download-extensions / build-extensions # actions below if [[ " $CATEGORY " == *" builders "* ]]; then + echo "" + echo "Running local build scripts..." + echo "" + set -x python pyomo/contrib/simplification/build.py --build-deps || exit 1 + set +x fi # Use Pyomo to download & compile binary extensions i=0 while /bin/true; do i=$[$i+1] + echo "" echo "Downloading pyomo extensions (attempt $i)" pyomo download-extensions $PYOMO_DOWNLOAD_ARGS if test $? == 0; then From 76d53de4d383cd03eadaa4997f4deecebec6d4f1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:54:19 -0600 Subject: [PATCH 1082/1178] Explicitly call out CWD when running configure --- pyomo/contrib/simplification/build.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 53133c2fb4e..b4bec63088a 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -27,7 +27,11 @@ def build_ginac_library(parallel=None, argv=None, env=None): sys.stdout.write("\n**** Building GiNaC library ****\n") - configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] + configure_cmd = [ + os.path.join('.', 'configure'), + '--prefix=' + PYOMO_CONFIG_DIR, + '--disable-static', + ] make_cmd = ['make'] if parallel: make_cmd.append(f'-j{parallel}') From 35b71f87a1d070b10553119c86489f99875a6587 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 20:17:31 -0600 Subject: [PATCH 1083/1178] Removing singletest from test_branches --- .github/workflows/test_branches.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d9c36e78fc4..5063571c65f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -95,14 +95,6 @@ jobs: PYENV: conda PACKAGES: openmpi mpi4py - - os: ubuntu-latest - python: '3.11' - other: /singletest - category: "-m 'neos or importtest'" - skip_doctest: 1 - TARGET: linux - PYENV: pip - - os: ubuntu-latest python: '3.10' other: /cython From f650f9645b27536e1ea45d36fc15af9b3fbc6f6e Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 08:35:51 -0600 Subject: [PATCH 1084/1178] More edits to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 954231f9f2a..922e072250e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Pyomo 6.7.2 (9 May 2024) - PyROS: Simplify custom domain validators (#3169) - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) - PyROS: Update solver timing system (#3198) + - simplification: New module for expression simplification using GiNaC or SymPy (#3088) ------------------------------------------------------------------------------- Pyomo 6.7.1 (21 Feb 2024) From 15b52dbabddcbc53a37ecc368d3a38d01e1482fa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 9 May 2024 09:27:25 -0600 Subject: [PATCH 1085/1178] Setting final deprecation version strings --- pyomo/common/dependencies.py | 6 +++--- pyomo/common/numeric_types.py | 2 +- pyomo/contrib/incidence_analysis/interface.py | 6 +++--- pyomo/contrib/parmest/parmest.py | 6 ++---- pyomo/core/base/__init__.py | 8 ++++---- pyomo/core/base/block.py | 2 +- pyomo/core/base/boolean_var.py | 4 ++-- pyomo/core/base/component.py | 6 +++--- pyomo/core/base/connector.py | 2 +- pyomo/core/base/constraint.py | 4 ++-- pyomo/core/base/expression.py | 6 +++--- pyomo/core/base/logical_constraint.py | 4 ++-- pyomo/core/base/objective.py | 4 ++-- pyomo/core/base/param.py | 2 +- pyomo/core/base/piecewise.py | 2 +- pyomo/core/base/set.py | 16 ++++++++-------- pyomo/core/base/sos.py | 2 +- pyomo/core/base/var.py | 4 ++-- pyomo/core/expr/numvalue.py | 2 +- pyomo/gdp/disjunct.py | 4 ++-- pyomo/mpec/complementarity.py | 2 +- pyomo/network/arc.py | 2 +- pyomo/network/port.py | 2 +- 23 files changed, 48 insertions(+), 50 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index ea9efe370f7..4c9e43002ef 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -611,7 +611,7 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2.dev0) + DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2) defer_import: bool, optional If True, then the attempted import is deferred until the first @@ -674,7 +674,7 @@ def attempt_import( if defer_check is not None: deprecation_warning( 'defer_check=%s is deprecated. Please use defer_import' % (defer_check,), - version='6.7.2.dev0', + version='6.7.2', ) assert defer_import is None defer_import = defer_check @@ -787,7 +787,7 @@ def _perform_import( @deprecated( "``declare_deferred_modules_as_importable()`` is deprecated. " "Use the :py:class:`declare_modules_as_importable` context manager.", - version='6.7.2.dev0', + version='6.7.2', ) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 8b48c77b5b2..2b63038e125 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -64,7 +64,7 @@ relocated_module_attribute( 'pyomo_constant_types', 'pyomo.common.numeric_types._pyomo_constant_types', - version='6.7.2.dev0', + version='6.7.2', msg="The pyomo_constant_types set will be removed in the future: the set " "contained only NumericConstant and _PythonCallbackFunctionID, and provided " "no meaningful value to clients or walkers. Users should likely handle " diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index b73ec17f36c..73d9722eb7e 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -891,9 +891,9 @@ def remove_nodes(self, variables=None, constraints=None): .. note:: - **Deprecation in Pyomo v6.7.2.dev0** + **Deprecation in Pyomo v6.7.2** - The pre-6.7.2.dev0 implementation of ``remove_nodes`` allowed variables and + The pre-6.7.2 implementation of ``remove_nodes`` allowed variables and constraints to remove to be specified in a single list. This made error checking difficult, and indeed, if invalid components were provided, we carried on silently instead of throwing an error or @@ -923,7 +923,7 @@ def remove_nodes(self, variables=None, constraints=None): if any(var in self._con_index_map for var in variables) or any( con in self._var_index_map for con in constraints ): - deprecation_warning(depr_msg, version="6.7.2.dev0") + deprecation_warning(depr_msg, version="6.7.2") # If we received variables/constraints in the same list, sort them. # Any unrecognized objects will be caught by _validate_input. for var in variables: diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 70f9de8b84c..41e7792570b 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -68,8 +68,6 @@ from pyomo.common.deprecation import deprecated from pyomo.common.deprecation import deprecation_warning -DEPRECATION_VERSION = '6.7.2.dev0' - parmest_available = numpy_available & pandas_available & scipy_available inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( @@ -338,7 +336,7 @@ def _deprecated_init( "You're using the deprecated parmest interface (model_function, " "data, theta_names). This interface will be removed in a future release, " "please update to the new parmest interface using experiment lists.", - version=DEPRECATION_VERSION, + version='6.7.2', ) self.pest_deprecated = _DeprecatedEstimator( model_function, @@ -1386,7 +1384,7 @@ def confidence_region_test( ################################ -@deprecated(version=DEPRECATION_VERSION) +@deprecated(version='6.7.2') def group_data(data, groupby_column_name, use_mean=None): """ Group data by scenario diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 2b21725d82f..6b295196864 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -163,13 +163,13 @@ ) # Historically, only a subset of "private" component data classes were imported here relocated_module_attribute( - f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2.dev0' + f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2' ) relocated_module_attribute( - f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2.dev0' + f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2' ) relocated_module_attribute( - f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2.dev0' + f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2' ) for _cdata in ( 'ConstraintData', @@ -179,7 +179,7 @@ 'ObjectiveData', ): relocated_module_attribute( - f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2.dev0' + f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2' ) del _cdata del relocated_module_attribute diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 2f5bdf85f6a..653809e0419 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1983,7 +1983,7 @@ def private_data(self, scope=None): class _BlockData(metaclass=RenamedClass): __renamed__new_class__ = BlockData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 67c06bdacce..db9a41fceda 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -252,12 +252,12 @@ def free(self): class _BooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralBooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Logical decision variables.") diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index d06b85dcdd4..966ce8c0737 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -477,7 +477,7 @@ def _pprint_base_impl( class _ComponentBase(metaclass=RenamedClass): __renamed__new_class__ = ComponentBase - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class Component(ComponentBase): @@ -663,7 +663,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): "use of this argument poses risks if the buffer contains " "names relative to different Blocks in the model hierarchy or " "a mixture of local and fully_qualified names.", - version='TODO', + version='6.4.1', ) name_buffer[id(self)] = ans return ans @@ -922,7 +922,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): "use of this argument poses risks if the buffer contains " "names relative to different Blocks in the model hierarchy or " "a mixture of local and fully_qualified names.", - version='TODO', + version='6.4.1', ) if id(self) in name_buffer: # Return the name if it is in the buffer diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index e383b52fc11..1363f5abd65 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -107,7 +107,7 @@ def _iter_vars(self): class _ConnectorData(metaclass=RenamedClass): __renamed__new_class__ = ConnectorData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index eb4af76fdc1..e12860991c2 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -577,12 +577,12 @@ def slack(self): class _ConstraintData(metaclass=RenamedClass): __renamed__new_class__ = ConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralConstraintData(metaclass=RenamedClass): __renamed__new_class__ = ConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("General constraint expressions.") diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 013c388e6e5..a5120759236 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -198,12 +198,12 @@ def __ipow__(self, other): class _ExpressionData(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralExpressionDataImpl(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class ExpressionData(NamedExpressionData, ComponentData): @@ -231,7 +231,7 @@ def __init__(self, expr=None, component=None): class _GeneralExpressionData(metaclass=RenamedClass): __renamed__new_class__ = ExpressionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 9584078307d..cc0780fd9bd 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -124,12 +124,12 @@ def get_value(self): class _LogicalConstraintData(metaclass=RenamedClass): __renamed__new_class__ = LogicalConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralLogicalConstraintData(metaclass=RenamedClass): __renamed__new_class__ = LogicalConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("General logical constraints.") diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index e388d25aab4..f1204f2a09c 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -145,12 +145,12 @@ def set_sense(self, sense): class _ObjectiveData(metaclass=RenamedClass): __renamed__new_class__ = ObjectiveData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralObjectiveData(metaclass=RenamedClass): __renamed__new_class__ = ObjectiveData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Expressions that are minimized or maximized.") diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 9af6a37de45..45de3286589 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -254,7 +254,7 @@ def _compute_polynomial_degree(self, result): class _ParamData(metaclass=RenamedClass): __renamed__new_class__ = ParamData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index efe500dbfb1..8c5f34d2b53 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -274,7 +274,7 @@ def __call__(self, x): class _PiecewiseData(metaclass=RenamedClass): __renamed__new_class__ = PiecewiseData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _SimpleSinglePiecewise(object): diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b9a2fe72e1d..8b7c2a246d6 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1179,12 +1179,12 @@ def __gt__(self, other): class _SetData(metaclass=RenamedClass): __renamed__new_class__ = SetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _SetDataBase(metaclass=RenamedClass): __renamed__new_class__ = SetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _FiniteSetMixin(object): @@ -1471,7 +1471,7 @@ def pop(self): class _FiniteSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _ScalarOrderedSetMixin(object): @@ -1736,7 +1736,7 @@ def ord(self, item): class _OrderedSetData(metaclass=RenamedClass): __renamed__new_class__ = OrderedSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class InsertionOrderSetData(OrderedSetData): @@ -1775,7 +1775,7 @@ def update(self, values): class _InsertionOrderSetData(metaclass=RenamedClass): __renamed__new_class__ = InsertionOrderSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _SortedSetMixin(object): @@ -1871,7 +1871,7 @@ def _sort(self): class _SortedSetData(metaclass=RenamedClass): __renamed__new_class__ = SortedSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' ############################################################################ @@ -2669,7 +2669,7 @@ def ranges(self): class _InfiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = InfiniteRangeSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class FiniteRangeSetData( @@ -2782,7 +2782,7 @@ def ord(self, item): class _FiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteRangeSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 4a8afb05d71..afd52c111bc 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -103,7 +103,7 @@ def set_items(self, variables, weights): class _SOSConstraintData(metaclass=RenamedClass): __renamed__new_class__ = SOSConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("SOS constraint expressions.") diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 8870fc5b09c..38d1d38a864 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -572,12 +572,12 @@ def _process_bound(self, val, bound_type): class _VarData(metaclass=RenamedClass): __renamed__new_class__ = VarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralVarData(metaclass=RenamedClass): __renamed__new_class__ = VarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Decision variables.") diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 3b335bd5fc4..96e2f50b3f8 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -47,7 +47,7 @@ relocated_module_attribute( 'pyomo_constant_types', 'pyomo.common.numeric_types._pyomo_constant_types', - version='6.7.2.dev0', + version='6.7.2', f_globals=globals(), msg="The pyomo_constant_types set will be removed in the future: the set " "contained only NumericConstant and _PythonCallbackFunctionID, and provided " diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index 658ead27783..637f55cbed1 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -450,7 +450,7 @@ def _activate_without_unfixing_indicator(self): class _DisjunctData(metaclass=RenamedClass): __renamed__new_class__ = DisjunctData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Disjunctive blocks.") @@ -627,7 +627,7 @@ def set_value(self, expr): class _DisjunctionData(metaclass=RenamedClass): __renamed__new_class__ = DisjunctionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Disjunction expressions.") diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index aa8db922145..26968ef9fca 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -181,7 +181,7 @@ def set_value(self, cc): class _ComplementarityData(metaclass=RenamedClass): __renamed__new_class__ = ComplementarityData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Complementarity conditions.") diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index 5e68f181a38..f2597b4c1bd 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -248,7 +248,7 @@ def _validate_ports(self, source, destination, ports): class _ArcData(metaclass=RenamedClass): __renamed__new_class__ = ArcData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Component used for connecting two Ports.") diff --git a/pyomo/network/port.py b/pyomo/network/port.py index ee5c915d8db..f6706dce644 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -287,7 +287,7 @@ def get_split_fraction(self, arc): class _PortData(metaclass=RenamedClass): __renamed__new_class__ = PortData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( From 28c158c9dbfce3928d8d14afbc8fab2bd9017d4a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 9 May 2024 09:27:53 -0600 Subject: [PATCH 1086/1178] Finalizing release information --- .coin-or/projDesc.xml | 4 ++-- CHANGELOG.md | 26 ++++++++++++++------------ RELEASE.md | 9 +++++++-- pyomo/version/info.py | 4 ++-- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index da977677d1f..073efd968a7 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.1 - 6.7.1 + 6.7.2 + 6.7.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 922e072250e..11b4ecbf785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ Pyomo 6.7.2 (9 May 2024) - Subprocess timeout update (#3183) - Solver Refactor - Bug fixes for various components (#3181, #3214, #3228) - NLv2: handle presolved independent linear subsystems (#3193) - - Update `LegacySolverWrapper` to be compatible with the `pyomo` script (#3202) + - Update `LegacySolverWrapper` compatibility with the `pyomo` script (#3202) - Fix mosek_direct to use putqconk instead of putqcon (#3199) - Check _skip_trivial_constraints before the constraint body (#3226) - Fix AMPL solver duplicate funcadd (#3206) @@ -43,37 +43,39 @@ Pyomo 6.7.2 (9 May 2024) - Set maxDiff=None on the base TestCase class (#3171) - Testing infrastructure updates (#3175) - Typos update for March 2024 (#3219) - - Add openmpi to testing environment to work around issue in mpi4py (#3236, #3239) + - Add openmpi to testing environment to resolve issue in mpi4py (#3236, #3239) - Skip black 24.4.1 due to a bug in the parser (#3247) - Skip tests on draft and WIP pull requests (#3223) - Update GHA to grab gurobipy from PyPI (#3254) - GDP - - Use private_data for all mappings between original and transformed components (#3166) + - Use private_data for all original / transformed component mappings (#3166) - Fix a bug in gdp.bigm transformation for nested GDPs (#3213) - Contributed Packages - - APPSI: Allow cmodel to handle non-mutable params in var and constraint bounds (#3182) + - APPSI: cmodel: handle non-mutable params in var / constraint bounds (#3182) - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) - APPSI: Add MAiNGO solver interface (#3165) - CP: Add SequenceVar and other logical expressions for scheduling (#3227) - DoE: Bug fixes (#3245) - - incidence_analysis: Improve performance of `solve_strongly_connected_components` for - models with named expressions (#3186) - - incidence_analysis: Add function to plot incidence graph in Dulmage-Mendelsohn order (#3207) - - incidence_analysis: Require variables and constraints to be specified separately in - `IncidenceGraphInterface.remove_nodes` (#3212) - - latex_printer: Resolve errors for set operations / multidimensional sets (#3177) + - iis: Add minimal intractable system infeasibility diagnostics (#3172) + - incidence_analysis: Improve `solve_strongly_connected_components` + performance for models with named expressions (#3186) + - incidence_analysis: Add function to plot incidence graph in + Dulmage-Mendelsohn order (#3207) + - incidence_analysis: Require variables and constraints to be specified + separately in `IncidenceGraphInterface.remove_nodes` (#3212) + - latex_printer: bugfix for set operations / multidimensional sets (#3177) - MindtPy: Add HiGHS support (#2971) - MindtPy: Add call_before_subproblem_solve callback (#3251) - Parmest: New UI using experiment lists (#3160) - piecewise: Add piecewise linear transformations (#3036) - - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) + - preprocessing: bugfix: intersect domains in variable aggregator (#3241) - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) - PyNumero: Include "inventory" in readme (#3248) - PyROS: Simplify custom domain validators (#3169) - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) - PyROS: Update solver timing system (#3198) - - simplification: New module for expression simplification using GiNaC or SymPy (#3088) + - simplification: expression simplification using GiNaC or SymPy (#3088) ------------------------------------------------------------------------------- Pyomo 6.7.1 (21 Feb 2024) diff --git a/RELEASE.md b/RELEASE.md index 9b101e0999a..b0228e53944 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -We are pleased to announce the release of Pyomo 6.7.1. +We are pleased to announce the release of Pyomo 6.7.2. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing @@ -10,9 +10,14 @@ The following are highlights of the 6.7 release series: - Removed support for Python 3.7 - New writer for converting linear models to matrix form - Improved handling of nested GDPs + - Redesigned user API for parameter estimation - New packages: - - latex_printer (print Pyomo models to a LaTeX compatible format) + - iis: new capability for identifying minimal intractable systems + - latex_printer: print Pyomo models to a LaTeX compatible format - contrib.solver: preview of redesigned solver interfaces + - simplification: simplify Pyomo expressions + - New solver interfaces + - MAiNGO: Mixed-integer nonlinear global optimization - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the diff --git a/pyomo/version/info.py b/pyomo/version/info.py index de2efe83fb6..b3538ad5868 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -27,8 +27,8 @@ major = 6 minor = 7 micro = 2 -releaselevel = 'invalid' -# releaselevel = 'final' +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': From d5e5136b317603f0fe0e25b1d31457e25388212f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 11:05:29 -0600 Subject: [PATCH 1087/1178] Unpinning numpy version requirement --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2e8cf1d5095..a125b02b2fe 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy<2.0.0', # Needed by autodoc for pynumero + 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -271,7 +271,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy<2.0.0', + 'numpy', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 167f2b14bf4c9d6a719f08a343128880bb04342c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 11:06:37 -0600 Subject: [PATCH 1088/1178] Resetting main for development (6.7.3.dev0) --- pyomo/version/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index b3538ad5868..2d50dfe7b5e 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 2 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 3 +releaselevel = 'invalid' +#releaselevel = 'final' serial = 0 if releaselevel == 'final': From 64a96147500cf0e71de8f955b3dfa92bf370799b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 11:10:11 -0600 Subject: [PATCH 1089/1178] Update info.py --- pyomo/version/info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 2d50dfe7b5e..36945e8e011 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -28,7 +28,7 @@ minor = 7 micro = 3 releaselevel = 'invalid' -#releaselevel = 'final' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': From ccb66b17630efcebbe719167dcd964e81ee00088 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 13:16:04 -0600 Subject: [PATCH 1090/1178] Add URL checking to MD and RST files --- .github/workflows/test_branches.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5063571c65f..7e5e5aff3ad 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,7 +47,17 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} From ecbe3193c47ed2a872b59b1ea75bce4308c03e58 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 13:24:16 -0600 Subject: [PATCH 1091/1178] Did this section break everything? --- .github/workflows/test_branches.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 7e5e5aff3ad..5063571c65f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,17 +47,7 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - - name: URL Checker - uses: urlstechie/urlchecker-action@0.0.34 - with: - # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst - # Choose whether to include file with no URLs in the prints. - print_all: false - # More verbose summary at the end of a run - verbose: true - # How many times to retry a failed request (defaults to 1) - retry_count: 3 + build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} From 9a68aab60ad71993b6a5ab89db07e869e4a95cdd Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:25:25 -0600 Subject: [PATCH 1092/1178] Fix broken links; ignore pyomo-jenkins --- .github/workflows/test_branches.yml | 14 +++++++++++++- .../pynumero/pynumero.sparse.block_vector.rst | 2 +- doc/OnlineDocs/contribution_guide.rst | 2 +- doc/OnlineDocs/modeling_extensions/dae.rst | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5063571c65f..20b5b869304 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,7 +47,19 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 + # Exclude Jenkins because it's behind a firewall + exclude_urls: https://pyomo-jenkins.sandia.gov build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst index 6e1dc1f20e5..c17d3d1df86 100644 --- a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst +++ b/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst @@ -77,7 +77,7 @@ NumPy compatible functions: * `numpy.arccos() `_ * `numpy.sinh() `_ * `numpy.cosh() `_ - * `numpy.abs() `_ + * `numpy.abs() `_ * `numpy.tanh() `_ * `numpy.arccosh() `_ * `numpy.arcsinh() `_ diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index b98dcc3d014..832bb70a78a 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -412,7 +412,7 @@ Including External Packages +++++++++++++++++++++++++++ The `pyomocontrib_simplemodel -`_ package +`_ package is derived from Pyomo, and it defines the class SimpleModel that illustrates how Pyomo can be used in a simple, less object-oriented manner. Specifically, this class mimics the modeling style supported diff --git a/doc/OnlineDocs/modeling_extensions/dae.rst b/doc/OnlineDocs/modeling_extensions/dae.rst index 703e83f4f14..ff0fb75e610 100644 --- a/doc/OnlineDocs/modeling_extensions/dae.rst +++ b/doc/OnlineDocs/modeling_extensions/dae.rst @@ -738,7 +738,7 @@ supported by CasADi. A list of available integrators for each package is given below. Please refer to the `SciPy `_ and `CasADi -`_ documentation directly for the most up-to-date information about +`_ documentation directly for the most up-to-date information about these packages and for more information about the various integrators and options. From 17646cc7bcca03ab4508afd9d698ad9efb6dcd70 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:30:59 -0600 Subject: [PATCH 1093/1178] We don't actually support 'external' contrib packages --- .github/workflows/test_branches.yml | 2 +- doc/OnlineDocs/contribution_guide.rst | 44 ++------------------------- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 20b5b869304..9fd15ebcb19 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -59,7 +59,7 @@ jobs: # How many times to retry a failed request (defaults to 1) retry_count: 3 # Exclude Jenkins because it's behind a firewall - exclude_urls: https://pyomo-jenkins.sandia.gov + exclude_urls: https://pyomo-jenkins.sandia.gov/ build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 832bb70a78a..9ad5bdfee0e 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -404,50 +404,10 @@ Contrib packages will be tested along with Pyomo. If test failures arise, then these packages will be disabled and an issue will be created to resolve these test failures. -The following two examples illustrate the two ways -that ``pyomo.contrib`` can be used to integrate third-party -contributions. - -Including External Packages -+++++++++++++++++++++++++++ - -The `pyomocontrib_simplemodel -`_ package -is derived from Pyomo, and it defines the class SimpleModel that -illustrates how Pyomo can be used in a simple, less object-oriented -manner. Specifically, this class mimics the modeling style supported -by `PuLP `_. - -While ``pyomocontrib_simplemodel`` can be installed and used separate -from Pyomo, this package is included in ``pyomo/contrib/simplemodel``. -This allows this package to be referenced as if were defined as a -subpackage of ``pyomo.contrib``. For example:: - - from pyomo.contrib.simplemodel import * - from math import pi - - m = SimpleModel() - - r = m.var('r', bounds=(0,None)) - h = m.var('h', bounds=(0,None)) - - m += 2*pi*r*(r + h) - m += pi*h*r**2 == 355 - - status = m.solve("ipopt") - -This example illustrates that a package can be distributed separate -from Pyomo while appearing to be included in the ``pyomo.contrib`` -subpackage. Pyomo requires a separate directory be defined under -``pyomo/contrib`` for each such package, and the Pyomo developer -team will approve the inclusion of third-party packages in this -manner. - - Contrib Packages within Pyomo +++++++++++++++++++++++++++++ -Third-party contributions can also be included directly within the +Third-party contributions can be included directly within the ``pyomo.contrib`` package. The ``pyomo/contrib/example`` package provides an example of how this can be done, including a directory for plugins and package tests. For example, this package can be @@ -465,7 +425,7 @@ import this package, but if an import failure occurs, Pyomo will silently ignore it. Otherwise, this pyomo package will be treated like any other. Specifically: -* Plugin classes defined in this package are loaded when `pyomo.environ` is loaded. +* Plugin classes defined in this package are loaded when ``pyomo.environ`` is loaded. * Tests in this package are run with other Pyomo tests. From 495c012943a17a272737454400a5ced9260e866a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:45:34 -0600 Subject: [PATCH 1094/1178] Add to PR file --- .github/workflows/test_pr_and_main.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a45fdd54f03..d0fe40c05bf 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -57,6 +57,19 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 + # Exclude Jenkins because it's behind a firewall + exclude_urls: https://pyomo-jenkins.sandia.gov/ build: From 293807c1a0953c8d5ea15069fc684ba11cfd70e5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:46:16 -0600 Subject: [PATCH 1095/1178] Apparently removed one line in branches file --- .github/workflows/test_branches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9fd15ebcb19..11a3fde709e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -61,6 +61,7 @@ jobs: # Exclude Jenkins because it's behind a firewall exclude_urls: https://pyomo-jenkins.sandia.gov/ + build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} runs-on: ${{ matrix.os }} From d6e66f398c721572af0a34f556dfc9de5a9c7134 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 12:46:57 -0600 Subject: [PATCH 1096/1178] Turn on .py files --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 11a3fde709e..4a5894bb6d3 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -51,7 +51,7 @@ jobs: uses: urlstechie/urlchecker-action@0.0.34 with: # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst + file_types: .md,.rst,.py # Choose whether to include file with no URLs in the prints. print_all: false # More verbose summary at the end of a run From 94ef83df768d512d350b11704e9136d60fda692c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:19:54 -0600 Subject: [PATCH 1097/1178] Fix broken URLs in py files --- examples/dae/ReactionKinetics.py | 5 ++++- pyomo/common/gsl.py | 4 ++-- pyomo/dataportal/plugins/db_table.py | 6 ++++-- pyomo/scripting/plugins/download.py | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/dae/ReactionKinetics.py b/examples/dae/ReactionKinetics.py index fa747cf8b21..ce097f7c748 100644 --- a/examples/dae/ReactionKinetics.py +++ b/examples/dae/ReactionKinetics.py @@ -304,7 +304,10 @@ def regression_model(): # Model & data from: # - # http://www.doiserbia.nb.rs/img/doi/0367-598X/2014/0367-598X1300037A.pdf + # https://doiserbia.nb.rs/img/doi/0367-598X/2014/0367-598X1300037A.pdf + # Almagrbi, A. M., Hatami, T., Glišić, S., & Orlović, A. (2014). + # Determination of kinetic parameters for complex transesterification + # reaction by standard optimisation methods. Hemijska industrija, 68(2), 149-159. # model = ConcreteModel() diff --git a/pyomo/common/gsl.py b/pyomo/common/gsl.py index 1c14b64bd70..96fab8623b3 100644 --- a/pyomo/common/gsl.py +++ b/pyomo/common/gsl.py @@ -23,8 +23,8 @@ ) def get_gsl(downloader): logger.info( - "As of February 9, 2023, AMPL GSL can no longer be downloaded\ - through download-extensions. Visit https://portal.ampl.com/\ + "As of February 9, 2023, AMPL GSL can no longer be downloaded \ + through download-extensions. Visit https://portal.ampl.com/ \ to download the AMPL GSL binaries." ) diff --git a/pyomo/dataportal/plugins/db_table.py b/pyomo/dataportal/plugins/db_table.py index a39705a6058..7c570757bf4 100644 --- a/pyomo/dataportal/plugins/db_table.py +++ b/pyomo/dataportal/plugins/db_table.py @@ -385,8 +385,10 @@ def __init__(self, filename=None, data=None): will override that in the file. """ - # ugh hardcoded strings. See following URL for info: - # http://publib.boulder.ibm.com/infocenter/idshelp/v10/index.jsp?topic=/com.ibm.odbc.doc/odbc58.htm + # These hardcoded strings were originally explained via a link + # to documentation that has since been moved and deleted. + # We have lost the historical knowledge as to why these strings + # are hardcoded as such. self.ODBC_DS_KEY = 'ODBC Data Sources' self.ODBC_INFO_KEY = 'ODBC' diff --git a/pyomo/scripting/plugins/download.py b/pyomo/scripting/plugins/download.py index eea858a737f..afe56988009 100644 --- a/pyomo/scripting/plugins/download.py +++ b/pyomo/scripting/plugins/download.py @@ -38,9 +38,9 @@ def _call_impl(self, args, unparsed, logger): self.downloader.cacert = args.cacert self.downloader.insecure = args.insecure logger.info( - "As of February 9, 2023, AMPL GSL can no longer be downloaded\ - through download-extensions. Visit https://portal.ampl.com/\ - to download the AMPL GSL binaries." + "As of February 9, 2023, AMPL GSL can no longer be downloaded \ + through download-extensions. Visit https://portal.ampl.com/ \ + to download the AMPL GSL binaries." ) for target in DownloadFactory: try: From b41f78290cd3b54de6099e5f3a39d871066e84b2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:22:27 -0600 Subject: [PATCH 1098/1178] Remove non-English journal title because it triggers spell check --- examples/dae/ReactionKinetics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dae/ReactionKinetics.py b/examples/dae/ReactionKinetics.py index ce097f7c748..2e474ae40d3 100644 --- a/examples/dae/ReactionKinetics.py +++ b/examples/dae/ReactionKinetics.py @@ -307,7 +307,7 @@ def regression_model(): # https://doiserbia.nb.rs/img/doi/0367-598X/2014/0367-598X1300037A.pdf # Almagrbi, A. M., Hatami, T., Glišić, S., & Orlović, A. (2014). # Determination of kinetic parameters for complex transesterification - # reaction by standard optimisation methods. Hemijska industrija, 68(2), 149-159. + # reaction by standard optimisation methods. # model = ConcreteModel() From c30a73a6aea981cc8d9db1f6ee30f6bfe1502f38 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:28:54 -0600 Subject: [PATCH 1099/1178] Missed a file; ignore a magic URL --- .github/workflows/test_branches.yml | 5 +++-- pyomo/solvers/plugins/solvers/CBCplugin.py | 21 ++++----------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 4a5894bb6d3..8ba04eec466 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -58,8 +58,9 @@ jobs: verbose: true # How many times to retry a failed request (defaults to 1) retry_count: 3 - # Exclude Jenkins because it's behind a firewall - exclude_urls: https://pyomo-jenkins.sandia.gov/ + # Exclude Jenkins because it's behind a firewall; ignore RTD because + # a magically-generated string is triggering a failure + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index f22fb117c8b..22ebf83f770 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -455,7 +455,6 @@ def process_logfile(self): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) if n_tokens > 1: - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3769 if n_tokens > 4 and tokens[:4] == ( 'Continuous', 'objective', @@ -539,7 +538,6 @@ def process_logfile(self): results.problem.name = results.problem.name.split('/')[-1] if '\\' in results.problem.name: results.problem.name = results.problem.name.split('\\')[-1] - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10840 elif tokens[0] == 'Presolve': if n_tokens > 9 and tokens[3] == 'rows,' and tokens[6] == 'columns': results.problem.number_of_variables = int(tokens[4]) - int( @@ -551,7 +549,6 @@ def process_logfile(self): results.problem.number_of_objectives = 1 elif n_tokens > 6 and tokens[6] == 'infeasible': soln.status = SolutionStatus.infeasible - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L11105 elif ( n_tokens > 11 and tokens[:2] == ('Problem', 'has') @@ -563,7 +560,6 @@ def process_logfile(self): results.problem.number_of_constraints = int(tokens[2]) results.problem.number_of_nonzeros = int(tokens[6][1:]) results.problem.number_of_objectives = 1 - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10814 elif ( n_tokens > 8 and tokens[:3] == ('Original', 'problem', 'has') @@ -579,7 +575,6 @@ def process_logfile(self): in ' '.join(tokens) ): results.problem.sense = maximize - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: results.solver.termination_condition = ( @@ -609,15 +604,12 @@ def process_logfile(self): 'solution': TerminationCondition.other, 'iterations': TerminationCondition.maxIterations, }.get(tokens[4], TerminationCondition.other) - # perhaps from https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L12318 elif n_tokens > 3 and tokens[2] == "Finished": soln.status = SolutionStatus.optimal optim_value = _float(tokens[4]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7904 elif n_tokens >= 3 and tokens[:2] == ('Objective', 'value:'): # parser for log file generetated with discrete variable optim_value = _float(tokens[2]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7904 elif n_tokens >= 4 and tokens[:4] == ( 'No', 'feasible', @@ -630,25 +622,19 @@ def process_logfile(self): lower_bound is None ): # Only use if not already found since this is to less decimal places results.problem.lower_bound = _float(tokens[2]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7918 elif tokens[0] == 'Gap:': # This is relative and only to 2 decimal places - could calculate explicitly using lower bound gap = _float(tokens[1]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7923 elif n_tokens > 2 and tokens[:2] == ('Enumerated', 'nodes:'): nodes = int(tokens[2]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7926 elif n_tokens > 2 and tokens[:2] == ('Total', 'iterations:'): results.solver.statistics.black_box.number_of_iterations = int( tokens[2] ) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7930 elif n_tokens > 3 and tokens[:3] == ('Time', '(CPU', 'seconds):'): results.solver.system_time = _float(tokens[3]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7933 elif n_tokens > 3 and tokens[:3] == ('Time', '(Wallclock', 'Seconds):'): results.solver.wallclock_time = _float(tokens[3]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10477 elif n_tokens > 4 and tokens[:4] == ( 'Total', 'time', @@ -833,9 +819,10 @@ def process_soln_file(self, results): n_tokens = len(tokens) # # These are the only header entries CBC will generate (identified via browsing CbcSolver.cpp) - # See https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp - # Search for (no integer solution - continuous used) Currently line 9912 as of rev2497 - # Note that since this possibly also covers old CBC versions, we shall not be removing any functionality, + # See https://github.com/coin-or/Cbc/tree/master/src + # Search for (no integer solution - continuous used) + # Note that since this possibly also covers old CBC versions, + # we shall not be removing any functionality, # even if it is not seen in the current revision # if not header_processed: From 155c5b209a184b9bf061878a3c4440501071a4bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:48:05 -0600 Subject: [PATCH 1100/1178] Add to PR test workflow --- .github/workflows/test_pr_and_main.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index d0fe40c05bf..8161e6186e4 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -61,15 +61,16 @@ jobs: uses: urlstechie/urlchecker-action@0.0.34 with: # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst + file_types: .md,.rst,.py # Choose whether to include file with no URLs in the prints. print_all: false # More verbose summary at the end of a run verbose: true # How many times to retry a failed request (defaults to 1) retry_count: 3 - # Exclude Jenkins because it's behind a firewall - exclude_urls: https://pyomo-jenkins.sandia.gov/ + # Exclude Jenkins because it's behind a firewall; ignore RTD because + # a magically-generated string is triggering a failure + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: From 909fd1e73cf04aa890616816cba789b8c0bb7d01 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 00:49:38 -0600 Subject: [PATCH 1101/1178] Support deferred final resolution of external function args --- pyomo/repn/plugins/nl_writer.py | 63 +++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index d4e7485cce4..763ed3cce0e 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -16,7 +16,7 @@ import os from collections import deque, defaultdict, namedtuple from contextlib import nullcontext -from itertools import filterfalse, product +from itertools import filterfalse, product, chain from math import log10 as _log10 from operator import itemgetter, attrgetter, setitem @@ -549,6 +549,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} + self.var_id_to_nl = None self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, @@ -1646,6 +1647,9 @@ def _categorize_vars(self, comp_list, linear_by_comp): Count of the number of components that each var appears in. """ + subexpression_cache = self.subexpression_cache + used_named_expressions = self.used_named_expressions + var_map = self.var_map all_linear_vars = set() all_nonlinear_vars = set() nnz_by_var = {} @@ -1673,9 +1677,14 @@ def _categorize_vars(self, comp_list, linear_by_comp): # Process the nonlinear portion of this component if expr_info.nonlinear: nonlinear_vars = set() - for _id in expr_info.nonlinear[1]: + _id_src = [expr_info.nonlinear[1]] + for _id in chain.from_iterable(_id_src): if _id in nonlinear_vars: continue + if _id not in var_map and _id not in used_named_expressions: + _sub_info = subexpression_cache[_id][1] + _id_src.append(_sub_info.nonlinear[1]) + continue if _id in linear_by_comp: nonlinear_vars.update(linear_by_comp[_id]) else: @@ -1945,7 +1954,22 @@ def _write_nl_expression(self, repn, include_const): # Add the constant to the NL expression. AMPL adds the # constant as the second argument, so we will too. nl = self.template.binary_sum + nl + self.template.const % repn.const - self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) + try: + self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) + except KeyError: + final_args = [] + for arg in args: + if arg in self.var_id_to_nl: + final_args.append(self.var_id_to_nl[arg]) + else: + _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( + self.visitor + ) + final_args.append( + _nl % tuple(map(self.var_id_to_nl.__getitem__, _ids)) + ) + self.ostream.write(nl % tuple(final_args)) + elif include_const: self.ostream.write(self.template.const % repn.const) else: @@ -2708,14 +2732,33 @@ def handle_external_function_node(visitor, node, *args): else: visitor.external_functions[func] = (len(visitor.external_functions), node._fcn) comment = f'\t#{node.local_name}' if visitor.symbolic_solver_labels else '' - nonlin = node_result_to_amplrepn(args[0]).compile_repn( - visitor, - visitor.template.external_fcn - % (visitor.external_functions[func][0], len(args), comment), + nl = visitor.template.external_fcn % ( + visitor.external_functions[func][0], + len(args), + comment, + ) + arg_ids = [] + for arg in args: + _id = id(arg) + arg_ids.append(_id) + named_exprs = set() + visitor.subexpression_cache[_id] = ( + arg, + AMPLRepn( + 0, + None, + node_result_to_amplrepn(arg).compile_repn( + visitor, named_exprs=named_exprs + ), + ), + (None, None, True), + ) + if not named_exprs: + named_exprs = None + return ( + _GENERAL, + AMPLRepn(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), ) - for arg in args[1:]: - nonlin = node_result_to_amplrepn(arg).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) _operator_handles = ExitNodeDispatcher( From 2a2fe4e3cc9c2625bcab35bfdd8a89483007a7bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 11:19:55 -0600 Subject: [PATCH 1102/1178] Attempt to propagate initial values for presolved variables --- pyomo/repn/plugins/nl_writer.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index bed073ea1d7..fc6db2c33fb 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -774,9 +774,10 @@ def write(self, model): # expressions, or when users provide superfluous variables in # the column ordering. var_bounds = {_id: v.bounds for _id, v in var_map.items()} + var_values = {_id: v.value for _id, v in var_map.items()} eliminated_cons, eliminated_vars = self._linear_presolve( - comp_by_linear_var, lcon_by_linear_nnz, var_bounds + comp_by_linear_var, lcon_by_linear_nnz, var_bounds, var_values ) del comp_by_linear_var del lcon_by_linear_nnz @@ -1472,7 +1473,7 @@ def write(self, model): # _init_lines = [ (var_idx, val if val.__class__ in int_float else float(val)) - for var_idx, val in enumerate(var_map[_id].value for _id in variables) + for var_idx, val in enumerate(map(var_values.__getitem__, variables)) if val is not None ] if scale_model: @@ -1746,7 +1747,9 @@ def _count_subexpression_occurrences(self): n_subexpressions[0] += 1 return n_subexpressions - def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): + def _linear_presolve( + self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds, var_values + ): eliminated_vars = {} eliminated_cons = set() if not self.config.linear_presolve: @@ -1819,7 +1822,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): ): _id, id2 = id2, _id coef, coef2 = coef2, coef - # substituting _id with a*x + b + # eliminating _id and replacing it with a*x + b a = -coef2 / coef x = id2 b = expr_info.const = (lb - expr_info.const) / coef @@ -1849,6 +1852,25 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): var_bounds[x] = x_lb, x_ub if x_lb == x_ub and x_lb is not None: fixed_vars.append(x) + # Given that we are eliminating a variable, we want to + # attempt to sanely resolve the initial variable values. + y_init = var_values[_id] + if y_init is not None: + # Y has a value + x_init = var_values[x] + if x_init is None: + # X does not; just use the one calculated from Y + x_init = (y_init - b) / a + else: + # X does too, use the average of the two values + x_init = (x_init + (y_init - b) / a) / 2.0 + # Ensure that the initial value respects the + # tightened bounds + if x_ub is not None and x_init > x_ub: + x_init = x_ub + if x_lb is not None and x_init < x_lb: + x_init = x_lb + var_values[x] = x_init eliminated_cons.add(con_id) else: break From cf8ac853e0d4cff39863f5f1548aa7427c1a7883 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 11:30:15 -0600 Subject: [PATCH 1103/1178] Reorder definitions to avoid NameError in some situations --- pyomo/core/expr/sympy_tools.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 6c184f0e4c4..b9381148544 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -10,12 +10,13 @@ # ___________________________________________________________________________ import operator import sys +from math import prod as _prod +import pyomo.core.expr as EXPR from pyomo.common import DeveloperError from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import attempt_import from pyomo.common.errors import NondifferentiableError -import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import value, native_types # @@ -113,18 +114,6 @@ def _configure_sympy(sympy, available): sympy, sympy_available = attempt_import('sympy', callback=_configure_sympy) -if sys.version_info[:2] < (3, 8): - - def _prod(args): - ans = 1 - for arg in args: - ans *= arg - return ans - -else: - from math import prod as _prod - - def _nondifferentiable(x): if type(x[1]) is tuple: # sympy >= 1.3 returns tuples (var, order) From fef9947f428125fa8d02887d8f596a87bd61c87c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 11:33:18 -0600 Subject: [PATCH 1104/1178] Remove unused import --- pyomo/core/expr/sympy_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index b9381148544..b1fd9f8245c 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ import operator -import sys from math import prod as _prod import pyomo.core.expr as EXPR From 02034790d49a49019bb5e04dedb60de1b8693b7f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 12:36:55 -0600 Subject: [PATCH 1105/1178] Add additional matplotlib import to fix error in community_detection plotting --- pyomo/common/dependencies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 4c9e43002ef..d30885e4860 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -965,6 +965,7 @@ def _finalize_matplotlib(module, available): import matplotlib.pyplot import matplotlib.pylab import matplotlib.backends + import matplotlib.cm # explicit import required for matplotlib>=3.9.0 def _finalize_numpy(np, available): From 8ae8348a3df281b032101255536f19fab8c049e3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 13:30:39 -0600 Subject: [PATCH 1106/1178] Change source of get_cmap; set minimum matplotlib version --- pyomo/common/dependencies.py | 1 - pyomo/contrib/community_detection/detection.py | 2 +- setup.py | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index d30885e4860..4c9e43002ef 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -965,7 +965,6 @@ def _finalize_matplotlib(module, available): import matplotlib.pyplot import matplotlib.pylab import matplotlib.backends - import matplotlib.cm # explicit import required for matplotlib>=3.9.0 def _finalize_numpy(np, available): diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index 0e2c3912e06..db3bb8f5a20 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -580,7 +580,7 @@ def visualize_model_graph( pos = nx.spring_layout(model_graph) # Define color_map - color_map = plt.cm.get_cmap('viridis', len(numbered_community_map)) + color_map = plt.get_cmap('viridis', len(numbered_community_map)) # Create the figure and draw the graph fig = plt.figure() diff --git a/setup.py b/setup.py index a125b02b2fe..6d28e4d184b 100644 --- a/setup.py +++ b/setup.py @@ -264,7 +264,9 @@ def __ne__(self, other): 'ipython', # contrib.viewer # Note: matplotlib 3.6.1 has bug #24127, which breaks # seaborn's histplot (triggering parmest failures) - 'matplotlib!=3.6.1', + # Note: minimum version from community_detection use of + # matplotlib.pyplot.get_cmap() + 'matplotlib>=3.6.0,!=3.6.1', # network, incidence_analysis, community_detection # Note: networkx 3.2 is Python>-3.9, but there is a broken # 3.2 package on conda-forge that will get implicitly From 8f1d61884edcff657b7f5da64cb72caba0a1880a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 15:45:25 -0600 Subject: [PATCH 1107/1178] Resolve definition ordering --- pyomo/core/expr/sympy_tools.py | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index b1fd9f8245c..d751ca35e5f 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -28,6 +28,25 @@ _functionMap = {} +def _nondifferentiable(x): + if type(x[1]) is tuple: + # sympy >= 1.3 returns tuples (var, order) + wrt = x[1][0] + else: + # early versions of sympy returned the bare var + wrt = x[1] + raise NondifferentiableError( + "The sub-expression '%s' is not differentiable with respect to %s" % (x[0], wrt) + ) + + +def _external_fcn(*x): + raise TypeError( + "Expressions containing external functions are not convertible to " + f"sympy expressions (found 'f{x}')" + ) + + def _configure_sympy(sympy, available): if not available: return @@ -113,25 +132,6 @@ def _configure_sympy(sympy, available): sympy, sympy_available = attempt_import('sympy', callback=_configure_sympy) -def _nondifferentiable(x): - if type(x[1]) is tuple: - # sympy >= 1.3 returns tuples (var, order) - wrt = x[1][0] - else: - # early versions of sympy returned the bare var - wrt = x[1] - raise NondifferentiableError( - "The sub-expression '%s' is not differentiable with respect to %s" % (x[0], wrt) - ) - - -def _external_fcn(*x): - raise TypeError( - "Expressions containing external functions are not convertible to " - f"sympy expressions (found 'f{x}')" - ) - - class PyomoSympyBimap(object): def __init__(self): self.pyomo2sympy = ComponentMap() From 5399127aa8926d185d880a1d0f282779686d6a77 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 17 May 2024 08:32:54 -0600 Subject: [PATCH 1108/1178] Update with newer links --- pyomo/dataportal/plugins/db_table.py | 7 +++---- pyomo/solvers/plugins/solvers/CBCplugin.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pyomo/dataportal/plugins/db_table.py b/pyomo/dataportal/plugins/db_table.py index 7c570757bf4..71fd499f725 100644 --- a/pyomo/dataportal/plugins/db_table.py +++ b/pyomo/dataportal/plugins/db_table.py @@ -385,10 +385,9 @@ def __init__(self, filename=None, data=None): will override that in the file. """ - # These hardcoded strings were originally explained via a link - # to documentation that has since been moved and deleted. - # We have lost the historical knowledge as to why these strings - # are hardcoded as such. + # Hardcoded string required here. + # See documentation: + # https://www.ibm.com/docs/en/informix-servers/12.10?topic=SSGU8G_12.1.0/com.ibm.odbc.doc/ids_odbc_062.html self.ODBC_DS_KEY = 'ODBC Data Sources' self.ODBC_INFO_KEY = 'ODBC' diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 22ebf83f770..4d49c5cc58d 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -455,6 +455,7 @@ def process_logfile(self): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) if n_tokens > 1: + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L3769 if n_tokens > 4 and tokens[:4] == ( 'Continuous', 'objective', @@ -538,6 +539,7 @@ def process_logfile(self): results.problem.name = results.problem.name.split('/')[-1] if '\\' in results.problem.name: results.problem.name = results.problem.name.split('\\')[-1] + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L10840 elif tokens[0] == 'Presolve': if n_tokens > 9 and tokens[3] == 'rows,' and tokens[6] == 'columns': results.problem.number_of_variables = int(tokens[4]) - int( @@ -549,6 +551,7 @@ def process_logfile(self): results.problem.number_of_objectives = 1 elif n_tokens > 6 and tokens[6] == 'infeasible': soln.status = SolutionStatus.infeasible + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L11105 elif ( n_tokens > 11 and tokens[:2] == ('Problem', 'has') @@ -560,6 +563,7 @@ def process_logfile(self): results.problem.number_of_constraints = int(tokens[2]) results.problem.number_of_nonzeros = int(tokens[6][1:]) results.problem.number_of_objectives = 1 + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L10814 elif ( n_tokens > 8 and tokens[:3] == ('Original', 'problem', 'has') @@ -575,6 +579,7 @@ def process_logfile(self): in ' '.join(tokens) ): results.problem.sense = maximize + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: results.solver.termination_condition = ( @@ -604,12 +609,15 @@ def process_logfile(self): 'solution': TerminationCondition.other, 'iterations': TerminationCondition.maxIterations, }.get(tokens[4], TerminationCondition.other) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L12318 elif n_tokens > 3 and tokens[2] == "Finished": soln.status = SolutionStatus.optimal optim_value = _float(tokens[4]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7904 elif n_tokens >= 3 and tokens[:2] == ('Objective', 'value:'): # parser for log file generetated with discrete variable optim_value = _float(tokens[2]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7904 elif n_tokens >= 4 and tokens[:4] == ( 'No', 'feasible', @@ -622,19 +630,25 @@ def process_logfile(self): lower_bound is None ): # Only use if not already found since this is to less decimal places results.problem.lower_bound = _float(tokens[2]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7918 elif tokens[0] == 'Gap:': # This is relative and only to 2 decimal places - could calculate explicitly using lower bound gap = _float(tokens[1]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7923 elif n_tokens > 2 and tokens[:2] == ('Enumerated', 'nodes:'): nodes = int(tokens[2]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7926 elif n_tokens > 2 and tokens[:2] == ('Total', 'iterations:'): results.solver.statistics.black_box.number_of_iterations = int( tokens[2] ) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7930 elif n_tokens > 3 and tokens[:3] == ('Time', '(CPU', 'seconds):'): results.solver.system_time = _float(tokens[3]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7933 elif n_tokens > 3 and tokens[:3] == ('Time', '(Wallclock', 'Seconds):'): results.solver.wallclock_time = _float(tokens[3]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L10477 elif n_tokens > 4 and tokens[:4] == ( 'Total', 'time', From 2d2057d24b457ef2e6cb00019d10376a14f21081 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 17 May 2024 15:29:34 -0600 Subject: [PATCH 1109/1178] Update/clarify comment and link --- pyomo/solvers/plugins/solvers/CBCplugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 4d49c5cc58d..96844e8ac59 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -832,12 +832,15 @@ def process_soln_file(self, results): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) # - # These are the only header entries CBC will generate (identified via browsing CbcSolver.cpp) - # See https://github.com/coin-or/Cbc/tree/master/src - # Search for (no integer solution - continuous used) - # Note that since this possibly also covers old CBC versions, - # we shall not be removing any functionality, - # even if it is not seen in the current revision + # These are the only header entries CBC will generate + # (identified via browsing CbcSolver.cpp) See + # https://github.com/coin-or/Cbc/tree/master/src/CbcSolver.cpp + # Search for "(no integer solution - continuous used)" + # (L10796 as of cb855c7) + # + # Note that since this possibly also covers old CBC + # versions, we shall not be removing any functionality, even + # if it is not seen in the current revision # if not header_processed: if tokens[0] == 'Optimal': From 7d25d1386117231391b26abefe653d267d2f5f3e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 17 May 2024 15:31:09 -0600 Subject: [PATCH 1110/1178] NFC: update doc/fix typo --- pyomo/solvers/plugins/solvers/CBCplugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 96844e8ac59..20876b07331 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -833,12 +833,12 @@ def process_soln_file(self, results): n_tokens = len(tokens) # # These are the only header entries CBC will generate - # (identified via browsing CbcSolver.cpp) See + # (identified via browsing CbcSolver.cpp). See # https://github.com/coin-or/Cbc/tree/master/src/CbcSolver.cpp # Search for "(no integer solution - continuous used)" # (L10796 as of cb855c7) # - # Note that since this possibly also covers old CBC + # Note that since this possibly also supports old CBC # versions, we shall not be removing any functionality, even # if it is not seen in the current revision # From 6f679844a8b4c79e2ef8e0a89237bc5b0ce7f52f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 17 May 2024 16:32:52 -0600 Subject: [PATCH 1111/1178] Deprecate pyomo.core.plugins.transform.model --- pyomo/core/plugins/transform/model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 7ee268a4292..9f370c96304 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -16,10 +16,17 @@ # because we may support an explicit matrix representation for models. # +from pyomo.common.deprecation import deprecated from pyomo.core.base import Objective, Constraint import array +@deprecated( + "to_standard_form() is deprecated. " + "Please use WriterFactory('compile_standard_form')", + version='6.7.3.dev0', + remove_in='6.8.0', +) def to_standard_form(self): """ Produces a standard-form representation of the model. Returns From 0eb5545e72bd5f0a8981a5ead7ac2788ee1985ef Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 19 May 2024 15:48:10 -0400 Subject: [PATCH 1112/1178] Fixed some bugs associated with specifying experiment design variables with no indices or a single (float) value/bound --- pyomo/contrib/doe/measurements.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index fd3962f7888..e7e16db6283 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -96,16 +96,21 @@ def add_variables( ) if values is not None: + # if a scalar (int or float) is given, set it as the value for all variables + if type(values) in native_numeric_types: + values = [values] * len(added_names) # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) - # if a scalar (int or float) is given, set it as the lower bound for all variables + if lower_bounds is not None: + # if a scalar (int or float) is given, set it as the lower bound for all variables if type(lower_bounds) in native_numeric_types: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) if upper_bounds is not None: + # if a scalar (int or float) is given, set it as the upper bound for all variables if type(upper_bounds) in native_numeric_types: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) @@ -129,7 +134,7 @@ def _generate_variable_names_with_indices( """ # first combine all indices into a list all_index_list = [] # contains all index lists - if indices: + if indices is not None: for index_pointer in indices: all_index_list.append(indices[index_pointer]) @@ -143,8 +148,11 @@ def _generate_variable_names_with_indices( added_names = [] # iterate over index combinations ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3] for index_instance in all_variable_indices: - var_name_index_string = var_name + "[" + var_name_index_string = var_name for i, idx in enumerate(index_instance): + # if i is the first index, open the [] + if i == 0: + var_name_index_string += "[" # use repr() is different from using str() # with repr(), "CA" is "CA", with str(), "CA" is CA. The first is not valid in our interface. var_name_index_string += str(idx) @@ -175,22 +183,31 @@ def _check_valid_input( """ assert isinstance(var_name, str), "var_name should be a string." - if time_index_position not in indices: + # check if time_index_position is in indices + if (indices is not None # ensure not None + and time_index_position is None # ensure not None + and time_index_position not in indices # ensure time_index_position is in indices + ): raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if values is not None and len(values) != len_indices: + if (values is not None # ensure not None + and not type(values) in native_numeric_types # skip this test if scalar (int or float) + and len(values) != len_indices + ): raise ValueError("Values is of different length with indices.") if ( lower_bounds is not None # ensure not None + and not type(lower_bounds) in native_numeric_types # skip this test if scalar (int or float) and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds is not None # ensure None + upper_bounds is not None # ensure not None + and not type(upper_bounds) in native_numeric_types # skip this test if scalar (int or float) and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): From 7b798ac989b8691232992f82d584c6c19e0f7d16 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 19 May 2024 21:31:36 -0400 Subject: [PATCH 1113/1178] Fixed bugs in sensitivity analysis. --- pyomo/contrib/doe/doe.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0fc3e8770fe..b00e24e18dd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -71,6 +71,7 @@ def __init__( prior_FIM=None, discretize_model=None, args=None, + logger_level=logging.INFO ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -101,6 +102,8 @@ def __init__( A user-specified ``function`` that discretizes the model. Only use with Pyomo.DAE, default=None args: Additional arguments for the create_model function. + logger_level: + Specify the level of the logger. Changer to logging.DEBUG for all messages. """ # parameters @@ -136,7 +139,7 @@ def __init__( # if print statements self.logger = logging.getLogger(__name__) - self.logger.setLevel(level=logging.INFO) + self.logger.setLevel(level=logger_level) def _check_inputs(self): """ @@ -727,6 +730,7 @@ def run_grid_search( store_optimality_as_csv=None, formula="central", step=0.001, + post_processing_function=None ): """ Enumerate through full grid search for any number of design variables; @@ -768,6 +772,10 @@ def run_grid_search( This option is only used for CalculationMode.sequential_finite. step: Sensitivity perturbation step size, a fraction between [0,1]. default is 0.001 + post_processing_function: + An optional function that executes after each solve of the grid search. + The function should take one input: the Pyomo model. This could be a plotting function. + Default is None. Returns ------- @@ -808,12 +816,18 @@ def run_grid_search( design_iter = self.design_vars.variable_names_value.copy() # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): - # if the element is a list, all design variables in this list share the same values - if isinstance(names, collections.abc.Sequence): + print("names =",names,"for i=",i) + if isinstance(names, str): + # if 'names' is simply a string, copy the new value + design_iter[names] = list(design_set_iter)[i] + elif isinstance(names, collections.abc.Sequence): + # if the element is a list, all design variables in this list share the same values for n in names: design_iter[n] = list(design_set_iter)[i] else: - design_iter[names] = list(design_set_iter)[i] + # otherwise just copy the value + # design_iter[names] = list(design_set_iter)[i] + raise NotImplementedError('You should not see this error message. Please report it to the Pyomo.DoE developers.') self.design_vars.variable_names_value = design_iter iter_timer = TicTocTimer() @@ -828,7 +842,7 @@ def run_grid_search( else: store_output_name = store_name + str(count) - if read_name: + if read_name is not None: read_input_name = read_name + str(count) else: read_input_name = None @@ -856,12 +870,16 @@ def run_grid_search( # give run information at each iteration self.logger.info('This is run %s out of %s.', count, total_count) - self.logger.info('The code has run %s seconds.', sum(time_set)) + self.logger.info('The code has run %s seconds.', round(sum(time_set),2)) self.logger.info( 'Estimated remaining time: %s seconds', - (sum(time_set) / (count + 1) * (total_count - count - 1)), + round(sum(time_set) / (count) * (total_count - count), 2), # need to check this math... it gives a negative number for the final count ) + if post_processing_function is not None: + # Call the post processing function + post_processing_function(self.model) + # the combined result object are organized as a dictionary, keys are a tuple of the design variable values, values are a result object result_combine[tuple(design_set_iter)] = result_iter From c2a0c8e24c66941c00eccba8efd6c48155bd7d0e Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 19 May 2024 21:52:40 -0400 Subject: [PATCH 1114/1178] Removed an extra print statement from debugging --- pyomo/contrib/doe/doe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b00e24e18dd..ddd091a24e2 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -816,7 +816,6 @@ def run_grid_search( design_iter = self.design_vars.variable_names_value.copy() # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): - print("names =",names,"for i=",i) if isinstance(names, str): # if 'names' is simply a string, copy the new value design_iter[names] = list(design_set_iter)[i] From 0f3a1ff9277ff97f9c66747f9bbbc4a4af207358 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 10:37:19 -0400 Subject: [PATCH 1115/1178] Updated useable for optional post-processing function in sensitivity analysis --- pyomo/contrib/doe/doe.py | 13 ++++++++++--- pyomo/contrib/doe/result.py | 16 ++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ddd091a24e2..ed44a18d262 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -423,6 +423,9 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # solve model square_result = self._solve_doe(mod, fix=True) + # save model from optional post processing function + self._square_model_from_compute_FIM = mod + if extract_single_model: mod_name = store_output + '.csv' dataframe = extract_single_model(mod, square_result) @@ -487,10 +490,10 @@ def _direct_kaug(self): mod = self.create_model(model_option=ModelOptionLib.parmest) # discretize if needed - if self.discretize_model: + if self.discretize_model is not None: mod = self.discretize_model(mod, block=False) - # add objective function + # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) # set ub and lb to parameters @@ -505,6 +508,10 @@ def _direct_kaug(self): # call k_aug get_dsdp function square_result = self._solve_doe(mod, fix=True) + + # save model from optional post processing function + self._square_model_from_compute_FIM = mod + dsdp_re, col = get_dsdp( mod, list(self.param.keys()), self.param, tee=self.tee_opt ) @@ -877,7 +884,7 @@ def run_grid_search( if post_processing_function is not None: # Call the post processing function - post_processing_function(self.model) + post_processing_function(self._square_model_from_compute_FIM) # the combined result object are organized as a dictionary, keys are a tuple of the design variable values, values are a result object result_combine[tuple(design_set_iter)] = result_iter diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 1593214c30a..d8ed343352a 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -549,7 +549,7 @@ def _curve1D( ax.scatter(x_range, y_range_A) ax.set_ylabel('$log_{10}$ Trace') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - A optimality') + plt.pyplot.title(title_text + ': A-optimality') plt.pyplot.show() # Draw D-optimality @@ -565,7 +565,7 @@ def _curve1D( ax.scatter(x_range, y_range_D) ax.set_ylabel('$log_{10}$ Determinant') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - D optimality') + plt.pyplot.title(title_text + ': D-optimality') plt.pyplot.show() # Draw E-optimality @@ -581,7 +581,7 @@ def _curve1D( ax.scatter(x_range, y_range_E) ax.set_ylabel('$log_{10}$ Minimal eigenvalue') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - E optimality') + plt.pyplot.title(title_text + ': E-optimality') plt.pyplot.show() # Draw Modified E-optimality @@ -597,7 +597,7 @@ def _curve1D( ax.scatter(x_range, y_range_ME) ax.set_ylabel('$log_{10}$ Condition number') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - Modified E optimality') + plt.pyplot.title(title_text + ': Modified E-optimality') plt.pyplot.show() def _heatmap( @@ -691,7 +691,7 @@ def _heatmap( im = ax.imshow(hes_a.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(trace(FIM))') - plt.pyplot.title(title_text + ' - A optimality') + plt.pyplot.title(title_text + ': A-optimality') plt.pyplot.show() # D-optimality @@ -712,7 +712,7 @@ def _heatmap( im = ax.imshow(hes_d.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(det(FIM))') - plt.pyplot.title(title_text + ' - D optimality') + plt.pyplot.title(title_text + ': D-optimality') plt.pyplot.show() # E-optimality @@ -733,7 +733,7 @@ def _heatmap( im = ax.imshow(hes_e.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(minimal eig(FIM))') - plt.pyplot.title(title_text + ' - E optimality') + plt.pyplot.title(title_text + ': E-optimality') plt.pyplot.show() # modified E-optimality @@ -754,5 +754,5 @@ def _heatmap( im = ax.imshow(hes_e2.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(cond(FIM))') - plt.pyplot.title(title_text + ' - Modified E-optimality') + plt.pyplot.title(title_text + ': Modified E-optimality') plt.pyplot.show() From 2f744561e7e26ff79b34c59f97dc283c7d438b37 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 12:50:52 -0400 Subject: [PATCH 1116/1178] Fixed maximizing log(trace) --- pyomo/contrib/doe/doe.py | 71 ++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ed44a18d262..8b110bbf0a4 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -988,19 +988,22 @@ def initialize_fim(m, j, d): initialize=identity_matrix, ) - # move the L matrix initial point to a dictionary - if type(self.L_initial) != type(None): - dict_cho = {} - for i, bu in enumerate(model.regression_parameters): - for j, un in enumerate(model.regression_parameters): - dict_cho[(bu, un)] = self.L_initial[i][j] - - # use the L dictionary to initialize L matrix - def init_cho(m, i, j): - return dict_cho[(i, j)] - # if cholesky, define L elements as variables - if self.Cholesky_option: + if self.Cholesky_option and self.objective_option == ObjectiveLib.det: + + # move the L matrix initial point to a dictionary + if type(self.L_initial) != type(None): + dict_cho = {} + # Loop over rows + for i, bu in enumerate(model.regression_parameters): + # Loop over columns + for j, un in enumerate(model.regression_parameters): + dict_cho[(bu, un)] = self.L_initial[i][j] + + # use the L dictionary to initialize L matrix + def init_cho(m, i, j): + return dict_cho[(i, j)] + # Define elements of Cholesky decomposition matrix as Pyomo variables and either # Initialize with L in L_initial if type(self.L_initial) != type(None): @@ -1095,14 +1098,22 @@ def fim_rule(m, p, q): def _add_objective(self, m): + small_number = 1E-10 + + # Assemble the FIM matrix. This is helpful for initialization! + fim = np.zeros((len(self.param), len(self.param))) + for i, bu in enumerate(m.regression_parameters): + for j, un in enumerate(m.regression_parameters): + # Copy value from Pyomo model into numpy array + fim[i][j] = m.fim[bu, un].value + + # Set lower bound to ensure diagonal elements are (almost) non-negative + # m.fim[bu, un].setlb(-small_number) + ### Initialize the Cholesky decomposition matrix - if self.Cholesky_option: + if self.Cholesky_option and self.objective_option == ObjectiveLib.det: + - # Assemble the FIM matrix - fim = np.zeros((len(self.param), len(self.param))) - for i, bu in enumerate(m.regression_parameters): - for j, un in enumerate(m.regression_parameters): - fim[i][j] = m.fim[bu, un].value # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) @@ -1115,10 +1126,10 @@ def _add_objective(self, m): # Compute the Cholesky decomposition of the FIM matrix L = np.linalg.cholesky(fim) - # Initialize the Cholesky matrix - for i, c in enumerate(m.regression_parameters): - for j, d in enumerate(m.regression_parameters): - m.L_ele[c, d].value = L[i, j] + # Initialize the Cholesky matrix + for i, c in enumerate(m.regression_parameters): + for j, d in enumerate(m.regression_parameters): + m.L_ele[c, d].value = L[i, j] def cholesky_imp(m, c, d): """ @@ -1173,7 +1184,7 @@ def det_general(m): ) return m.det == det_perm - if self.Cholesky_option: + if self.Cholesky_option and self.objective_option == ObjectiveLib.det: m.cholesky_cons = pyo.Constraint( m.regression_parameters, m.regression_parameters, rule=cholesky_imp ) @@ -1181,16 +1192,26 @@ def det_general(m): expr=2 * sum(pyo.log(m.L_ele[j, j]) for j in m.regression_parameters), sense=pyo.maximize, ) - # if not cholesky but determinant, calculating det and evaluate the OBJ with det + elif self.objective_option == ObjectiveLib.det: + # if not cholesky but determinant, calculating det and evaluate the OBJ with det + m.det = pyo.Var(initialize=np.linalg.det(fim), bounds=(small_number, None)) m.det_rule = pyo.Constraint(rule=det_general) m.Obj = pyo.Objective(expr=pyo.log(m.det), sense=pyo.maximize) - # if not determinant or cholesky, calculating the OBJ with trace + elif self.objective_option == ObjectiveLib.trace: + # if not determinant or cholesky, calculating the OBJ with trace + m.trace = pyo.Var(initialize=np.trace(fim), bounds=(small_number, None)) m.trace_rule = pyo.Constraint(rule=trace_calc) m.Obj = pyo.Objective(expr=pyo.log(m.trace), sense=pyo.maximize) + #m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) + elif self.objective_option == ObjectiveLib.zero: + # add dummy objective function m.Obj = pyo.Objective(expr=0) + else: + # something went wrong! + raise ValueError("Objective option not recognized. Please contact the developers as you should not see this error.") return m From 2ae726dbba0c80b73dd73a67ed50c811515c51ea Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 13:31:18 -0400 Subject: [PATCH 1117/1178] Ran black. --- pyomo/contrib/doe/doe.py | 40 ++++++++++++++++++------------- pyomo/contrib/doe/measurements.py | 21 +++++++++------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 8b110bbf0a4..3243bec6e1c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -71,7 +71,7 @@ def __init__( prior_FIM=None, discretize_model=None, args=None, - logger_level=logging.INFO + logger_level=logging.INFO, ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -508,10 +508,10 @@ def _direct_kaug(self): # call k_aug get_dsdp function square_result = self._solve_doe(mod, fix=True) - + # save model from optional post processing function self._square_model_from_compute_FIM = mod - + dsdp_re, col = get_dsdp( mod, list(self.param.keys()), self.param, tee=self.tee_opt ) @@ -737,7 +737,7 @@ def run_grid_search( store_optimality_as_csv=None, formula="central", step=0.001, - post_processing_function=None + post_processing_function=None, ): """ Enumerate through full grid search for any number of design variables; @@ -833,7 +833,9 @@ def run_grid_search( else: # otherwise just copy the value # design_iter[names] = list(design_set_iter)[i] - raise NotImplementedError('You should not see this error message. Please report it to the Pyomo.DoE developers.') + raise NotImplementedError( + 'You should not see this error message. Please report it to the Pyomo.DoE developers.' + ) self.design_vars.variable_names_value = design_iter iter_timer = TicTocTimer() @@ -876,10 +878,14 @@ def run_grid_search( # give run information at each iteration self.logger.info('This is run %s out of %s.', count, total_count) - self.logger.info('The code has run %s seconds.', round(sum(time_set),2)) + self.logger.info( + 'The code has run %s seconds.', round(sum(time_set), 2) + ) self.logger.info( 'Estimated remaining time: %s seconds', - round(sum(time_set) / (count) * (total_count - count), 2), # need to check this math... it gives a negative number for the final count + round( + sum(time_set) / (count) * (total_count - count), 2 + ), # need to check this math... it gives a negative number for the final count ) if post_processing_function is not None: @@ -990,7 +996,7 @@ def initialize_fim(m, j, d): # if cholesky, define L elements as variables if self.Cholesky_option and self.objective_option == ObjectiveLib.det: - + # move the L matrix initial point to a dictionary if type(self.L_initial) != type(None): dict_cho = {} @@ -1003,7 +1009,7 @@ def initialize_fim(m, j, d): # use the L dictionary to initialize L matrix def init_cho(m, i, j): return dict_cho[(i, j)] - + # Define elements of Cholesky decomposition matrix as Pyomo variables and either # Initialize with L in L_initial if type(self.L_initial) != type(None): @@ -1098,7 +1104,7 @@ def fim_rule(m, p, q): def _add_objective(self, m): - small_number = 1E-10 + small_number = 1e-10 # Assemble the FIM matrix. This is helpful for initialization! fim = np.zeros((len(self.param), len(self.param))) @@ -1113,8 +1119,6 @@ def _add_objective(self, m): ### Initialize the Cholesky decomposition matrix if self.Cholesky_option and self.objective_option == ObjectiveLib.det: - - # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) @@ -1192,26 +1196,28 @@ def det_general(m): expr=2 * sum(pyo.log(m.L_ele[j, j]) for j in m.regression_parameters), sense=pyo.maximize, ) - + elif self.objective_option == ObjectiveLib.det: # if not cholesky but determinant, calculating det and evaluate the OBJ with det m.det = pyo.Var(initialize=np.linalg.det(fim), bounds=(small_number, None)) m.det_rule = pyo.Constraint(rule=det_general) m.Obj = pyo.Objective(expr=pyo.log(m.det), sense=pyo.maximize) - + elif self.objective_option == ObjectiveLib.trace: # if not determinant or cholesky, calculating the OBJ with trace m.trace = pyo.Var(initialize=np.trace(fim), bounds=(small_number, None)) m.trace_rule = pyo.Constraint(rule=trace_calc) m.Obj = pyo.Objective(expr=pyo.log(m.trace), sense=pyo.maximize) - #m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) - + # m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) + elif self.objective_option == ObjectiveLib.zero: # add dummy objective function m.Obj = pyo.Objective(expr=0) else: # something went wrong! - raise ValueError("Objective option not recognized. Please contact the developers as you should not see this error.") + raise ValueError( + "Objective option not recognized. Please contact the developers as you should not see this error." + ) return m diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index e7e16db6283..47df09d27c1 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -102,7 +102,6 @@ def add_variables( # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) - if lower_bounds is not None: # if a scalar (int or float) is given, set it as the lower bound for all variables if type(lower_bounds) in native_numeric_types: @@ -184,22 +183,27 @@ def _check_valid_input( assert isinstance(var_name, str), "var_name should be a string." # check if time_index_position is in indices - if (indices is not None # ensure not None - and time_index_position is None # ensure not None - and time_index_position not in indices # ensure time_index_position is in indices + if ( + indices is not None # ensure not None + and time_index_position is None # ensure not None + and time_index_position + not in indices # ensure time_index_position is in indices ): raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if (values is not None # ensure not None - and not type(values) in native_numeric_types # skip this test if scalar (int or float) + if ( + values is not None # ensure not None + and not type(values) + in native_numeric_types # skip this test if scalar (int or float) and len(values) != len_indices ): raise ValueError("Values is of different length with indices.") if ( lower_bounds is not None # ensure not None - and not type(lower_bounds) in native_numeric_types # skip this test if scalar (int or float) + and not type(lower_bounds) + in native_numeric_types # skip this test if scalar (int or float) and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): @@ -207,7 +211,8 @@ def _check_valid_input( if ( upper_bounds is not None # ensure not None - and not type(upper_bounds) in native_numeric_types # skip this test if scalar (int or float) + and not type(upper_bounds) + in native_numeric_types # skip this test if scalar (int or float) and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): From 405ac567a4c2d793adf79400b77609466bbc226d Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 13:46:46 -0400 Subject: [PATCH 1118/1178] Added note to help with additional debugging. --- pyomo/contrib/doe/measurements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 47df09d27c1..229fb7f7830 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -182,6 +182,7 @@ def _check_valid_input( """ assert isinstance(var_name, str), "var_name should be a string." + # debugging note: what is an integer versus a list versus a dictionary here? # check if time_index_position is in indices if ( indices is not None # ensure not None From 50c8ffc71be56ce8569bb3cca54d82122d58507a Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 20 May 2024 14:17:07 -0400 Subject: [PATCH 1119/1178] Enact design variable bounds --- pyomo/contrib/doe/doe.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 3243bec6e1c..f84147841c6 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -662,6 +662,13 @@ def fix1(mod, s): con_name = "con" + name mod.add_component(con_name, pyo.Constraint(mod.scenario, expr=fix1)) + # Add user-defined design variable bounds + cuid = pyo.ComponentUID(name) + design_var_global = cuid.find_component_on(mod) + # Set the lower and upper bounds of the design variables + design_var_global.setlb(self.design_vars.lower_bounds[name]) + design_var_global.setub(self.design_vars.upper_bounds[name]) + return mod def _finite_calculation(self, output_record): From dbd3d8f0c8f753881fa66faf9783e2395487121f Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 20 May 2024 14:19:46 -0400 Subject: [PATCH 1120/1178] Enacted user-passed model arguments --- pyomo/contrib/doe/doe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index f84147841c6..02ac1908b5d 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -70,7 +70,7 @@ def __init__( solver=None, prior_FIM=None, discretize_model=None, - args=None, + args={}, logger_level=logging.INFO, ): """ @@ -487,7 +487,7 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): def _direct_kaug(self): # create model - mod = self.create_model(model_option=ModelOptionLib.parmest) + mod = self.create_model(model_option=ModelOptionLib.parmest, **self.args) # discretize if needed if self.discretize_model is not None: @@ -612,7 +612,7 @@ def _create_block(self): ) # Allow user to self-define complex design variables - self.create_model(mod=mod, model_option=ModelOptionLib.stage1) + self.create_model(mod=mod, model_option=ModelOptionLib.stage1, **self.args) # Fix parameter values in the copy of the stage1 model (if they exist) for par in self.param: @@ -632,11 +632,11 @@ def block_build(b, s): theta_initialize = self.scenario_data.scenario[s] # Add model on block with theta values self.create_model( - mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize + mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize, **self.args, ) else: # Otherwise add model on block without theta values - self.create_model(mod=b, model_option=ModelOptionLib.stage2) + self.create_model(mod=b, model_option=ModelOptionLib.stage2, **self.args) # fix parameter values to perturbed values for par in self.param: From 9ded80f156e3f1e0139d98bf4e036dc30c100e05 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:12:25 -0600 Subject: [PATCH 1121/1178] Resolve KeyError with export_nonlinear_variables --- pyomo/repn/plugins/nl_writer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index fc6db2c33fb..7a991a989f0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -851,6 +851,7 @@ def write(self, model): if _id not in var_map: var_map[_id] = _v var_bounds[_id] = _v.bounds + var_values[_id] = _v.value con_vars_nonlinear.add(_id) con_nnz = sum(con_nnz_by_var.values()) From 5badeba53f1b47685c885fff48df3a6e887ef718 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:13:18 -0600 Subject: [PATCH 1122/1178] Fix error when ressolving constant arguments to external functions --- pyomo/repn/plugins/nl_writer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 7a991a989f0..331c45b78a5 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1683,8 +1683,9 @@ def _categorize_vars(self, comp_list, linear_by_comp): if _id in nonlinear_vars: continue if _id not in var_map and _id not in used_named_expressions: - _sub_info = subexpression_cache[_id][1] - _id_src.append(_sub_info.nonlinear[1]) + _sub_info = subexpression_cache[_id][1].nonlinear + if _sub_info: + _id_src.append(_sub_info[1]) continue if _id in linear_by_comp: nonlinear_vars.update(linear_by_comp[_id]) From 6c1ced9ab4c922dca1b8823911ceea910f853acd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:14:24 -0600 Subject: [PATCH 1123/1178] Add test for external functions whose arguments presolve to constants --- pyomo/repn/tests/ampl/test_nlv2.py | 120 ++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index f2378e56b4c..784a277c118 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -25,6 +25,7 @@ from pyomo.common.dependencies import numpy, numpy_available from pyomo.common.errors import MouseTrap +from pyomo.common.gsl import find_GSL from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -2311,7 +2312,7 @@ def test_discrete_var_tabulation(self): def test_presolve_fixes_nl_defined_variables(self): # This tests a workaround for a bug in the ASL where defined - # variables with nonstant expressions in the NL portion are not + # variables with constant expressions in the NL portion are not # evaluated correctly. m = ConcreteModel() m.x = Var() @@ -2471,6 +2472,123 @@ def test_presolve_fixes_nl_defined_variables(self): J1 2 #c2 0 1 2 -1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_fixes_nl_exernal_function(self): + # This tests a workaround for a bug in the ASL where external + # functions with constant argument expressions are not + # evaluated correctly. + DLL = find_GSL() + if not DLL: + self.skipTest("Could not find the amplgsl.dll library") + + m = ConcreteModel() + m.hypot = ExternalFunction(library=DLL, function="gsl_hypot") + m.p = Param(initialize=1, mutable=True) + m.x = Var(bounds=(None, 3)) + m.y = Var(bounds=(3, None)) + m.z = Var(initialize=1) + m.o = Objective(expr=m.z**2 * m.hypot(m.p * m.x, m.p + m.y) ** 2) + m.c = Constraint(expr=m.x == m.y) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=False, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 3 1 1 0 1 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 3 0 #nonlinear vars in constraints, objectives, both + 0 1 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 2 3 #nonzeros in Jacobian, obj. gradient + 1 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +F0 1 -1 gsl_hypot +C0 #c +n0 +O0 0 #o +o2 #* +o5 #^ +v0 #z +n2 +o5 #^ +f0 2 #hypot +v1 #x +o0 #+ +v2 #y +n1 +n2 +x1 #initial guess +0 1 #z +r #1 ranges (rhs's) +4 0 #c +b #3 bounds (on variables) +3 #z +1 3 #x +2 3 #y +k2 #intermediate Jacobian column lengths +0 +1 +J0 2 #c +1 1 +2 -1 +G0 3 #o +0 0 +1 0 +2 0 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 1 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 1 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +F0 1 -1 gsl_hypot +O0 0 #o +o2 #* +o5 #^ +v0 #z +n2 +o5 #^ +f0 2 #hypot +n3 +n4 +n2 +x1 #initial guess +0 1 #z +r #0 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #o +0 0 """, OUT.getvalue(), ) From 26c0e4bf3d13a9fbd707a5c7809ef7be7e680567 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:27:49 -0600 Subject: [PATCH 1124/1178] NFC: apply black --- pyomo/repn/tests/ampl/test_nlv2.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 784a277c118..97c93ad1649 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2496,10 +2496,7 @@ def test_presolve_fixes_nl_exernal_function(self): OUT = io.StringIO() nl_writer.NLWriter().write( - m, - OUT, - symbolic_solver_labels=True, - linear_presolve=False, + m, OUT, symbolic_solver_labels=True, linear_presolve=False ) self.assertEqual( *nl_diff( @@ -2553,10 +2550,7 @@ def test_presolve_fixes_nl_exernal_function(self): OUT = io.StringIO() nl_writer.NLWriter().write( - m, - OUT, - symbolic_solver_labels=True, - linear_presolve=True, + m, OUT, symbolic_solver_labels=True, linear_presolve=True ) self.assertEqual( *nl_diff( From 5d13a38bd6d2f699412f447b85101e4351e0823b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 21 May 2024 07:17:14 -0400 Subject: [PATCH 1125/1178] Removed bounds on FIM diagonal --- pyomo/contrib/doe/doe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 02ac1908b5d..412f387f8b5 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1121,7 +1121,8 @@ def _add_objective(self, m): fim[i][j] = m.fim[bu, un].value # Set lower bound to ensure diagonal elements are (almost) non-negative - # m.fim[bu, un].setlb(-small_number) + # if i == j: + # m.fim[bu, un].setlb(-small_number) ### Initialize the Cholesky decomposition matrix if self.Cholesky_option and self.objective_option == ObjectiveLib.det: From d2b75f36f0dca7e6fc243719e3e7db542ca23dfd Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 21 May 2024 07:21:14 -0400 Subject: [PATCH 1126/1178] Added suggestion for future improvement --- pyomo/contrib/doe/doe.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 412f387f8b5..f5c053876bd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1082,6 +1082,11 @@ def read_prior(m, i, j): model.regression_parameters, model.regression_parameters, rule=read_prior ) + # TODO: explore exploiting the symmetry of the FIM matrix + # The off-diagonal elements are symmetric, thus only half of the elements need to be calculated + # Syntax challenge: determine the order of p and q, i.e., if p > q, then replace with + # equality constraint fim[p, q] == fim[q, p] + def fim_rule(m, p, q): """ m: Pyomo model From f616aded6a98327ad057e517f6bad5cfef7bcffc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:08:19 -0600 Subject: [PATCH 1127/1178] Remove subexpression_order from AMPLRepnVisitor --- pyomo/contrib/incidence_analysis/config.py | 2 -- pyomo/repn/plugins/nl_writer.py | 23 +++++++--------------- pyomo/repn/tests/ampl/test_nlv2.py | 2 -- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 2a7734ba433..9fac48c8a26 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -130,7 +130,6 @@ def get_config_from_kwds(**kwds): and kwds.get("_ampl_repn_visitor", None) is None ): subexpression_cache = {} - subexpression_order = [] external_functions = {} var_map = {} used_named_expressions = set() @@ -143,7 +142,6 @@ def get_config_from_kwds(**kwds): amplvisitor = AMPLRepnVisitor( text_nl_template, subexpression_cache, - subexpression_order, external_functions, var_map, used_named_expressions, diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 331c45b78a5..04d8c42540f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -544,7 +544,7 @@ def __init__(self, ostream, rowstream, colstream, config): else: self.template = text_nl_template self.subexpression_cache = {} - self.subexpression_order = [] + self.subexpression_order = None # set to [] later self.external_functions = {} self.used_named_expressions = set() self.var_map = {} @@ -553,7 +553,6 @@ def __init__(self, ostream, rowstream, colstream, config): self.visitor = AMPLRepnVisitor( self.template, self.subexpression_cache, - self.subexpression_order, self.external_functions, self.var_map, self.used_named_expressions, @@ -802,7 +801,7 @@ def write(self, model): # Filter out any unused named expressions self.subexpression_order = list( - filter(self.used_named_expressions.__contains__, self.subexpression_order) + filter(self.used_named_expressions.__contains__, self.subexpression_cache) ) # linear contribution by (constraint, objective, variable) component. @@ -824,10 +823,7 @@ def write(self, model): # We need to categorize the named subexpressions first so that # we know their linear / nonlinear vars when we encounter them # in constraints / objectives - self._categorize_vars( - map(self.subexpression_cache.__getitem__, self.subexpression_order), - linear_by_comp, - ) + self._categorize_vars(self.subexpression_cache.values(), linear_by_comp) n_subexpressions = self._count_subexpression_occurrences() obj_vars_linear, obj_vars_nonlinear, obj_nnz_by_var = self._categorize_vars( objectives, linear_by_comp @@ -2672,8 +2668,10 @@ def handle_named_expression_node(visitor, node, arg1): nl_info = list(expression_source) visitor.subexpression_cache[sub_id] = (sub_node, sub_repn, nl_info) # It is important that the NL subexpression comes before the - # main named expression: - visitor.subexpression_order.append(sub_id) + # main named expression: re-insert the original named + # expression (so that the nonlinear sub_node comes first + # when iterating over subexpression_cache) + visitor.subexpression_cache[_id] = visitor.subexpression_cache.pop(_id) else: nl_info = expression_source else: @@ -2716,11 +2714,6 @@ def handle_named_expression_node(visitor, node, arg1): else: return (_CONSTANT, repn.const) - # Defer recording this _id until after we know that this repn will - # not be directly substituted (and to ensure that the NL fragment is - # added to the order first). - visitor.subexpression_order.append(_id) - return (_GENERAL, repn.duplicate()) @@ -2989,7 +2982,6 @@ def __init__( self, template, subexpression_cache, - subexpression_order, external_functions, var_map, used_named_expressions, @@ -3000,7 +2992,6 @@ def __init__( super().__init__() self.template = template self.subexpression_cache = subexpression_cache - self.subexpression_order = subexpression_order self.external_functions = external_functions self.active_expression_source = None self.var_map = var_map diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 97c93ad1649..0aa9fab96f9 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -58,7 +58,6 @@ def __init__(self, symbolic=False): else: self.template = nl_writer.text_nl_template self.subexpression_cache = {} - self.subexpression_order = [] self.external_functions = {} self.var_map = {} self.used_named_expressions = set() @@ -67,7 +66,6 @@ def __init__(self, symbolic=False): self.visitor = nl_writer.AMPLRepnVisitor( self.template, self.subexpression_cache, - self.subexpression_order, self.external_functions, self.var_map, self.used_named_expressions, From b87325a7df44d222763e5ded71be0d8fede8aa9b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:09:54 -0600 Subject: [PATCH 1128/1178] Revert some previous changes that were not necessary: because we work leaf-to-root, unnamed subexpressions will be handled correctly --- pyomo/repn/plugins/nl_writer.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 04d8c42540f..74a7251a1fe 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1644,9 +1644,6 @@ def _categorize_vars(self, comp_list, linear_by_comp): Count of the number of components that each var appears in. """ - subexpression_cache = self.subexpression_cache - used_named_expressions = self.used_named_expressions - var_map = self.var_map all_linear_vars = set() all_nonlinear_vars = set() nnz_by_var = {} @@ -1674,15 +1671,9 @@ def _categorize_vars(self, comp_list, linear_by_comp): # Process the nonlinear portion of this component if expr_info.nonlinear: nonlinear_vars = set() - _id_src = [expr_info.nonlinear[1]] - for _id in chain.from_iterable(_id_src): + for _id in expr_info.nonlinear[1]: if _id in nonlinear_vars: continue - if _id not in var_map and _id not in used_named_expressions: - _sub_info = subexpression_cache[_id][1].nonlinear - if _sub_info: - _id_src.append(_sub_info[1]) - continue if _id in linear_by_comp: nonlinear_vars.update(linear_by_comp[_id]) else: From 254ca0486965f9f02e0e6f37e342a0d56989e7a1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:10:27 -0600 Subject: [PATCH 1129/1178] Avoid error cor constant AMPLRepn objects --- pyomo/repn/plugins/nl_writer.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 74a7251a1fe..93f0832acf0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1691,12 +1691,13 @@ def _categorize_vars(self, comp_list, linear_by_comp): expr_info.linear = dict.fromkeys(nonlinear_vars, 0) all_nonlinear_vars.update(nonlinear_vars) - # Update the count of components that each variable appears in - for v in expr_info.linear: - if v in nnz_by_var: - nnz_by_var[v] += 1 - else: - nnz_by_var[v] = 1 + if expr_info.linear: + # Update the count of components that each variable appears in + for v in expr_info.linear: + if v in nnz_by_var: + nnz_by_var[v] += 1 + else: + nnz_by_var[v] = 1 # Record all nonzero variable ids for this component linear_by_comp[id(comp_info[0])] = expr_info.linear # Linear models (or objectives) are common. Avoid the set From 0fecf80f6c0a6ee170413d5a41211025be39ef99 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:10:55 -0600 Subject: [PATCH 1130/1178] Resolve potential information leak when duplicating AMPLRepn --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 93f0832acf0..b0fb7099894 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2077,7 +2077,7 @@ def duplicate(self): ans.const = self.const ans.linear = None if self.linear is None else dict(self.linear) ans.nonlinear = self.nonlinear - ans.named_exprs = self.named_exprs + ans.named_exprs = None if self.named_exprs is None else set(self.named_exprs) return ans def compile_repn(self, visitor, prefix='', args=None, named_exprs=None): From 7a9a83eb6e808a4ca437dad51ac46108114de295 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:11:24 -0600 Subject: [PATCH 1131/1178] Additional error checking when substituting simple named expressions --- pyomo/repn/plugins/nl_writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index b0fb7099894..5344348d0a0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2702,7 +2702,8 @@ def handle_named_expression_node(visitor, node, arg1): if expression_source[2]: if repn.linear: - return (_MONOMIAL, next(iter(repn.linear)), 1) + assert len(repn.linear) == 1 and not repn.const + return (_MONOMIAL,) + next(iter(repn.linear.items())) else: return (_CONSTANT, repn.const) From 69b7d3011f23aedf3631e8c46a4438e97ee90cba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:11:56 -0600 Subject: [PATCH 1132/1178] Resolve error when collecting named expressions used in external functions --- pyomo/repn/plugins/nl_writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 5344348d0a0..e174ce796c6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2747,10 +2747,10 @@ def handle_external_function_node(visitor, node, *args): comment, ) arg_ids = [] + named_exprs = set() for arg in args: _id = id(arg) arg_ids.append(_id) - named_exprs = set() visitor.subexpression_cache[_id] = ( arg, AMPLRepn( @@ -2762,8 +2762,8 @@ def handle_external_function_node(visitor, node, *args): ), (None, None, True), ) - if not named_exprs: - named_exprs = None + if not named_exprs: + named_exprs = None return ( _GENERAL, AMPLRepn(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), From 0ceedc61ae67589866694e1b09527c4570196dd2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:12:44 -0600 Subject: [PATCH 1133/1178] Track changes in AMPLRepnVisitor (handling of external functions) --- pyomo/contrib/incidence_analysis/incidence.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 96cbf77c47d..030ee2b0f79 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -83,6 +83,17 @@ def _get_incident_via_standard_repn( def _get_incident_via_ampl_repn(expr, linear_only, visitor): + def _nonlinear_var_id_collector(idlist): + for _id in idlist: + if _id in visitor.subexpression_cache: + info = visitor.subexpression_cache[_id][1] + if info.nonlinear: + yield from _nonlinear_var_id_collector(info.nonlinear[1]) + if info.linear: + yield from _nonlinear_var_id_collector(info.linear) + else: + yield _id + var_map = visitor.var_map orig_activevisitor = AMPLRepn.ActiveVisitor AMPLRepn.ActiveVisitor = visitor @@ -91,13 +102,13 @@ def _get_incident_via_ampl_repn(expr, linear_only, visitor): finally: AMPLRepn.ActiveVisitor = orig_activevisitor - nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] nonlinear_var_id_set = set() unique_nonlinear_var_ids = [] - for v_id in nonlinear_var_ids: - if v_id not in nonlinear_var_id_set: - nonlinear_var_id_set.add(v_id) - unique_nonlinear_var_ids.append(v_id) + if repn.nonlinear: + for v_id in _nonlinear_var_id_collector(repn.nonlinear[1]): + if v_id not in nonlinear_var_id_set: + nonlinear_var_id_set.add(v_id) + unique_nonlinear_var_ids.append(v_id) nonlinear_vars = [var_map[v_id] for v_id in unique_nonlinear_var_ids] linear_only_vars = [ From b0cbeeeb8b218b54eacce61305a49ead6a1449e5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 21 May 2024 15:36:08 -0600 Subject: [PATCH 1134/1178] Update test_pr_and_main.yml --- .github/workflows/test_pr_and_main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 8161e6186e4..bdf1f7e1aa5 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -86,6 +86,11 @@ jobs: other: [""] category: [""] + # win/3.8 conda builds no longer work due to environment not being able + # to resolve. We are skipping it now. + exclude: + - os: windows-latest + python: 3.8 include: - os: ubuntu-latest TARGET: linux From 6be83b57981238ba94c81e4582819e340b7fbdc6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 22 May 2024 22:00:30 -0600 Subject: [PATCH 1135/1178] NFC: clarify comment wrt string args --- pyomo/repn/plugins/nl_writer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e174ce796c6..fa8a49c345b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2714,9 +2714,12 @@ def handle_external_function_node(visitor, node, *args): func = node._fcn._function # There is a special case for external functions: these are the only # expressions that can accept string arguments. As we currently pass - # these as 'precompiled' general NL fragments, the normal trap for - # constant subexpressions will miss constant external function calls - # that contain strings. We will catch that case here. + # these as 'precompiled' GENERAL AMPLRepns, the normal trap for + # constant subexpressions will miss string arguments. We will catch + # that case here by looking for NL fragments with no variable + # references. Note that the NL fragment is NOT the raw string + # argument that we want to evaluate: the raw string is in the + # `const` field. if all( arg[0] is _CONSTANT or (arg[0] is _GENERAL and arg[1].nl and not arg[1].nl[1]) for arg in args From 04313dcd8a990880287f6350e22ad033ec49850d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 22 May 2024 22:00:55 -0600 Subject: [PATCH 1136/1178] fix detection of variables replaces with expressions --- pyomo/repn/plugins/nl_writer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index fa8a49c345b..0a6f9f3da30 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1924,7 +1924,10 @@ def _linear_presolve( if not info.nonlinear: continue nl, args = info.nonlinear - if not args or any(vid not in eliminated_vars for vid in args): + if not args or any( + vid not in eliminated_vars or eliminated_vars[vid].linear + for vid in args + ): continue # Ideally, we would just evaluate the named expression. # However, there might be a linear portion of the named From c85d38cfedac6393b407abc0a160f0a6c1d648bf Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 09:10:06 -0400 Subject: [PATCH 1137/1178] Implemented most suggestions from JS. Still need to debug failing test and some string manipulation logic. --- pyomo/contrib/doe/doe.py | 49 ++++++++++++++++++------------- pyomo/contrib/doe/measurements.py | 3 ++ 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index f5c053876bd..0c37364642b 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -70,7 +70,7 @@ def __init__( solver=None, prior_FIM=None, discretize_model=None, - args={}, + args=None, logger_level=logging.INFO, ): """ @@ -103,7 +103,7 @@ def __init__( args: Additional arguments for the create_model function. logger_level: - Specify the level of the logger. Changer to logging.DEBUG for all messages. + Specify the level of the logger. Change to logging.DEBUG for all messages. """ # parameters @@ -114,6 +114,9 @@ def __init__( self.design_name = design_vars.variable_names self.design_vars = design_vars self.create_model = create_model + + if args is None: + args = {} self.args = args # create the measurement information object @@ -145,7 +148,7 @@ def _check_inputs(self): """ Check if the prior FIM is N*N matrix, where N is the number of parameter """ - if type(self.prior_FIM) != type(None): + if self.prior_FIM is not None: if np.shape(self.prior_FIM)[0] != np.shape(self.prior_FIM)[1]: raise ValueError('Found wrong prior information matrix shape.') elif np.shape(self.prior_FIM)[0] != len(self.param): @@ -980,10 +983,12 @@ def initialize_jac(m, i, j): ) if self.fim_initial is not None: - dict_fim_initialize = {} - for i, bu in enumerate(model.regression_parameters): - for j, un in enumerate(model.regression_parameters): - dict_fim_initialize[(bu, un)] = self.fim_initial[i][j] + dict_fim_initialize = { + (bu, un): self.fim_initial[i][j] + for i, bu in enumerate(model.regression_parameters) + for j, un in enumerate(model.regression_parameters) + + } def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] @@ -1005,13 +1010,12 @@ def initialize_fim(m, j, d): if self.Cholesky_option and self.objective_option == ObjectiveLib.det: # move the L matrix initial point to a dictionary - if type(self.L_initial) != type(None): - dict_cho = {} - # Loop over rows - for i, bu in enumerate(model.regression_parameters): - # Loop over columns - for j, un in enumerate(model.regression_parameters): - dict_cho[(bu, un)] = self.L_initial[i][j] + if self.L_initial is not None: + dict_cho = { + (bu, un): self.L_initial[i][j] + for i, bu in enumerate(model.regression_parameters) + for j, un in enumerate(model.regression_parameters) + } # use the L dictionary to initialize L matrix def init_cho(m, i, j): @@ -1019,7 +1023,7 @@ def init_cho(m, i, j): # Define elements of Cholesky decomposition matrix as Pyomo variables and either # Initialize with L in L_initial - if type(self.L_initial) != type(None): + if self.L_initial is not None: model.L_ele = pyo.Var( model.regression_parameters, model.regression_parameters, @@ -1070,10 +1074,11 @@ def jacobian_rule(m, p, n): # A constraint to calculate elements in Hessian matrix # transfer prior FIM to be Expressions - fim_initial_dict = {} - for i, bu in enumerate(model.regression_parameters): - for j, un in enumerate(model.regression_parameters): - fim_initial_dict[(bu, un)] = self.prior_FIM[i][j] + fim_initial_dict = { + (bu, un): self.prior_FIM[i][j] + for i, bu in enumerate(model.regression_parameters) + for j, un in enumerate(model.regression_parameters) + } def read_prior(m, i, j): return fim_initial_dict[(i, j)] @@ -1119,6 +1124,10 @@ def _add_objective(self, m): small_number = 1e-10 # Assemble the FIM matrix. This is helpful for initialization! + # + # Suggestion from JS: "It might be more efficient to form the NP array in one shot + # (from a list or using fromiter), and then reshaping to the 2-D matrix" + # fim = np.zeros((len(self.param), len(self.param))) for i, bu in enumerate(m.regression_parameters): for j, un in enumerate(m.regression_parameters): @@ -1228,7 +1237,7 @@ def det_general(m): m.Obj = pyo.Objective(expr=0) else: # something went wrong! - raise ValueError( + raise DeveloperError( "Objective option not recognized. Please contact the developers as you should not see this error." ) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 229fb7f7830..ff40aa6143f 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -148,6 +148,9 @@ def _generate_variable_names_with_indices( # iterate over index combinations ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3] for index_instance in all_variable_indices: var_name_index_string = var_name + # + # Suggestion from JS: "Can you re-use name_repr and index_repr from pyomo.core.base.component_namer here?" + # for i, idx in enumerate(index_instance): # if i is the first index, open the [] if i == 0: From 459b593b8790945d2fd552a49f05ad20c7232d5b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 09:52:32 -0400 Subject: [PATCH 1138/1178] Added more tests. --- pyomo/contrib/doe/measurements.py | 3 +- pyomo/contrib/doe/tests/test_fim_doe.py | 124 ++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index ff40aa6143f..1b47c78c65c 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -183,7 +183,8 @@ def _check_valid_input( """ Check if the measurement information provided are valid to use. """ - assert isinstance(var_name, str), "var_name should be a string." + if not isinstance(var_name, str): + raise TypeError("Variable name must be a string.") # debugging note: what is an integer versus a list versus a dictionary here? # check if time_index_position is in indices diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 31d250f0d10..a41ad552228 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -38,18 +38,128 @@ class TestMeasurementError(unittest.TestCase): - def test(self): - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] + + def test_with_time_plus_one_extra_index(self): + """ This tests confirms the typical usage with a time index plus one extra index. + + This test should execute without throwing any errors. + + """ + + MeasurementVariables().add_variables( + "C", + indices={0: ['A', 'B', 'C'], 1: [0, 0.5, 1.0]}, + time_index_position=1 + ) + + def test_with_time_plus_two_extra_indices(self): + """ This tests confirms the typical usage with a time index plus two extra indices. + + This test should execute without throwing any errors. + + """ + + MeasurementVariables().add_variables( + "C", + indices={0: ['A', 'B', 'C'], # species + 1: [0, 0.5, 1.0], # time + 2: [1, 2, 3]}, # position + time_index_position=1 + ) + + def test_time_index_position_out_of_bounds(self): + """ This test confirms that an error is thrown when the time index position is out of bounds. + + """ + + # if time index is not in indices, an value error is thrown. + with self.assertRaises(ValueError): + MeasurementVariables().add_variables( + "C", + indices={0: ['CA', 'CB', 'CC'], # species + 1: [0, 0.5, 1.0],}, # time + time_index_position=2 # this is out of bounds + ) + + def test_single_measurement_variable(self): + """ This test confirms we can specify a single measurement variable without + specifying the indices. + + The test should execute with no errors. + """ + measurements = MeasurementVariables() + measurements.add_variables( + "HelloWorld", + indices=None, + time_index_position=None) + + def test_without_time_index(self): + """ This test confirms we can add a measurement variable without specifying the time index. + + The test should execute with no errors. + + """ variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ['CA', 'CB', 'CC']} # specify the indices + # no time index + # measurement object measurements = MeasurementVariables() - # if time index is not in indices, an value error is thrown. - with self.assertRaises(ValueError): - measurements.add_variables( - variable_name, indices=indices, time_index_position=2 + measurements.add_variables( + variable_name, indices=indices, time_index_position=None ) + + def test_only_time_index(self): + """ This test confirms we can add a measurement variable without specifying the variable name. + + The test should execute with no errors. + + """ + + MeasurementVariables().add_variables( + "HelloWorld", # name of the variable + indices={0: [0, 0.5, 1.0]}, + time_index_position=0 + ) + + def test_with_no_measurement_name(self): + """ This test confirms that an error is thrown when None is used as the measurement name. + """ + + with self.assertRaises(TypeError): + MeasurementVariables().add_variables( + None, + indices={0: [0, 0.5, 1.0]}, + time_index_position=0 + ) + + def test_with_non_string_measurement_name(self): + """ This test confirms that an error is thrown when a non-string is used as the measurement name. + + """ + + with self.assertRaises(TypeError): + MeasurementVariables().add_variables( + 1, + indices={0: [0, 0.5, 1.0]}, + time_index_position=0 + ) + + def test_non_integer_index_keys(self): + """ This test confirms that strings can be used as keys for specifying the indices. + + Warning: it is possible this usage breaks something else in Pyomo.DoE. + There may be an implicit assumption that the order of the keys must match the order + of the indices in the Pyomo model. + + """ + + MeasurementVariables().add_variables( + "C", + indices={"species": ['CA', 'CB', 'CC'], "time": [0, 0.5, 1.0]}, + time_index_position="time" + ) class TestDesignError(unittest.TestCase): def test(self): From a1667ad79e9e12131a5fe76c49b9fa08f2fc4167 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 10:43:50 -0400 Subject: [PATCH 1139/1178] Fixed mistake in test logic. --- pyomo/contrib/doe/measurements.py | 10 +++++----- pyomo/contrib/doe/tests/test_fim_doe.py | 11 ++++------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 1b47c78c65c..31a9dc19dbb 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -190,13 +190,13 @@ def _check_valid_input( # check if time_index_position is in indices if ( indices is not None # ensure not None - and time_index_position is None # ensure not None + and time_index_position is not None # ensure not None and time_index_position - not in indices # ensure time_index_position is in indices + not in indices.keys() # ensure time_index_position is in indices ): raise ValueError("time index cannot be found in indices.") - # if given a list, check if bounds have the same length with flattened variable + # if given a list, check if values have the same length with flattened variable if ( values is not None # ensure not None and not type(values) @@ -212,7 +212,7 @@ def _check_valid_input( and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): - raise ValueError("Lowerbounds is of different length with indices.") + raise ValueError("Lowerbounds have a different length with indices.") if ( upper_bounds is not None # ensure not None @@ -221,7 +221,7 @@ def _check_valid_input( and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): - raise ValueError("Upperbounds is of different length with indices.") + raise ValueError("Upperbounds have a different length with indices.") class MeasurementVariables(VariablesWithIndices): diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index a41ad552228..d2431f7bd45 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -99,14 +99,11 @@ def test_without_time_index(self): The test should execute with no errors. """ - variable_name = "C" - indices = {0: ['CA', 'CB', 'CC']} # specify the indices - # no time index - # measurement object - measurements = MeasurementVariables() - measurements.add_variables( - variable_name, indices=indices, time_index_position=None + MeasurementVariables().add_variables( + "C", + indices= {0: ['CA', 'CB', 'CC']}, # species as only index + time_index_position=None # no time index ) def test_only_time_index(self): From 62af537940e163e52863e9f0954308263dbb8b7a Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 10:53:09 -0400 Subject: [PATCH 1140/1178] Implemented caching suggestion from JS; ran black. --- pyomo/contrib/doe/doe.py | 68 ++--- pyomo/contrib/doe/result.py | 174 ++++++------- pyomo/contrib/doe/scenario.py | 2 +- pyomo/contrib/doe/tests/test_example.py | 2 +- pyomo/contrib/doe/tests/test_fim_doe.py | 237 +++++++++--------- .../contrib/doe/tests/test_reactor_example.py | 12 +- 6 files changed, 245 insertions(+), 250 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0c37364642b..9356cce360b 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -114,7 +114,7 @@ def __init__( self.design_name = design_vars.variable_names self.design_vars = design_vars self.create_model = create_model - + if args is None: args = {} self.args = args @@ -150,9 +150,9 @@ def _check_inputs(self): """ if self.prior_FIM is not None: if np.shape(self.prior_FIM)[0] != np.shape(self.prior_FIM)[1]: - raise ValueError('Found wrong prior information matrix shape.') + raise ValueError("Found wrong prior information matrix shape.") elif np.shape(self.prior_FIM)[0] != len(self.param): - raise ValueError('Found wrong prior information matrix shape.') + raise ValueError("Found wrong prior information matrix shape.") def stochastic_program( self, @@ -411,7 +411,7 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # if measurements are provided if read_output: - with open(read_output, 'rb') as f: + with open(read_output, "rb") as f: output_record = pickle.load(f) f.close() jac = self._finite_calculation(output_record) @@ -430,7 +430,7 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): self._square_model_from_compute_FIM = mod if extract_single_model: - mod_name = store_output + '.csv' + mod_name = store_output + ".csv" dataframe = extract_single_model(mod, square_result) dataframe.to_csv(mod_name) @@ -452,10 +452,10 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): output_record[s] = output_iter - output_record['design'] = self.design_values + output_record["design"] = self.design_values if store_output: - f = open(store_output, 'wb') + f = open(store_output, "wb") pickle.dump(output_record, f) f.close() @@ -537,7 +537,7 @@ def _direct_kaug(self): dsdp_extract.append(dsdp_array[kaug_no]) except: # k_aug does not provide value for fixed variables - self.logger.debug('The variable is fixed: %s', mname) + self.logger.debug("The variable is fixed: %s", mname) # produce the sensitivity for fixed variables zero_sens = np.zeros(len(self.param)) # for fixed variables, the sensitivity are a zero vector @@ -611,7 +611,7 @@ def _create_block(self): # Determine if create_model takes theta as an optional input pass_theta_to_initialize = ( - 'theta' in inspect.getfullargspec(self.create_model).args + "theta" in inspect.getfullargspec(self.create_model).args ) # Allow user to self-define complex design variables @@ -635,11 +635,16 @@ def block_build(b, s): theta_initialize = self.scenario_data.scenario[s] # Add model on block with theta values self.create_model( - mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize, **self.args, + mod=b, + model_option=ModelOptionLib.stage2, + theta=theta_initialize, + **self.args, ) else: # Otherwise add model on block without theta values - self.create_model(mod=b, model_option=ModelOptionLib.stage2, **self.args) + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, **self.args + ) # fix parameter values to perturbed values for par in self.param: @@ -831,27 +836,31 @@ def run_grid_search( # generate the design variable dictionary needed for running compute_FIM # first copy value from design_values design_iter = self.design_vars.variable_names_value.copy() + + # convert to a list and cache + list_design_set_iter = list(design_set_iter) + # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): if isinstance(names, str): # if 'names' is simply a string, copy the new value - design_iter[names] = list(design_set_iter)[i] + design_iter[names] = list_design_set_iter[i] elif isinstance(names, collections.abc.Sequence): # if the element is a list, all design variables in this list share the same values for n in names: - design_iter[n] = list(design_set_iter)[i] + design_iter[n] = list_design_set_iter[i] else: # otherwise just copy the value # design_iter[names] = list(design_set_iter)[i] raise NotImplementedError( - 'You should not see this error message. Please report it to the Pyomo.DoE developers.' + "You should not see this error message. Please report it to the Pyomo.DoE developers." ) self.design_vars.variable_names_value = design_iter iter_timer = TicTocTimer() - self.logger.info('=======Iteration Number: %s =====', count + 1) + self.logger.info("=======Iteration Number: %s =====", count + 1) self.logger.debug( - 'Design variable values of this iteration: %s', design_iter + "Design variable values of this iteration: %s", design_iter ) iter_timer.tic(msg=None) # generate store name @@ -887,12 +896,12 @@ def run_grid_search( time_set.append(iter_t) # give run information at each iteration - self.logger.info('This is run %s out of %s.', count, total_count) + self.logger.info("This is run %s out of %s.", count, total_count) self.logger.info( - 'The code has run %s seconds.', round(sum(time_set), 2) + "The code has run %s seconds.", round(sum(time_set), 2) ) self.logger.info( - 'Estimated remaining time: %s seconds', + "Estimated remaining time: %s seconds", round( sum(time_set) / (count) * (total_count - count), 2 ), # need to check this math... it gives a negative number for the final count @@ -907,11 +916,11 @@ def run_grid_search( except: self.logger.warning( - ':::::::::::Warning: Cannot converge this run.::::::::::::' + ":::::::::::Warning: Cannot converge this run.::::::::::::" ) count += 1 failed_count += 1 - self.logger.warning('failed count:', failed_count) + self.logger.warning("failed count:", failed_count) result_combine[tuple(design_set_iter)] = None # For user's access @@ -925,7 +934,7 @@ def run_grid_search( store_optimality_name=store_optimality_as_csv, ) - self.logger.info('Overall wall clock time [s]: %s', sum(time_set)) + self.logger.info("Overall wall clock time [s]: %s", sum(time_set)) return figure_draw_object @@ -987,7 +996,6 @@ def initialize_jac(m, i, j): (bu, un): self.fim_initial[i][j] for i, bu in enumerate(model.regression_parameters) for j, un in enumerate(model.regression_parameters) - } def initialize_fim(m, j, d): @@ -1012,7 +1020,7 @@ def initialize_fim(m, j, d): # move the L matrix initial point to a dictionary if self.L_initial is not None: dict_cho = { - (bu, un): self.L_initial[i][j] + (bu, un): self.L_initial[i][j] for i, bu in enumerate(model.regression_parameters) for j, un in enumerate(model.regression_parameters) } @@ -1124,8 +1132,8 @@ def _add_objective(self, m): small_number = 1e-10 # Assemble the FIM matrix. This is helpful for initialization! - # - # Suggestion from JS: "It might be more efficient to form the NP array in one shot + # + # Suggestion from JS: "It might be more efficient to form the NP array in one shot # (from a list or using fromiter), and then reshaping to the 2-D matrix" # fim = np.zeros((len(self.param), len(self.param))) @@ -1279,10 +1287,10 @@ def _fix_design(self, m, design_val, fix_opt=True, optimize_option=None): def _get_default_ipopt_solver(self): """Default solver""" - solver = SolverFactory('ipopt') - solver.options['linear_solver'] = 'ma57' - solver.options['halt_on_ampl_error'] = 'yes' - solver.options['max_iter'] = 3000 + solver = SolverFactory("ipopt") + solver.options["linear_solver"] = "ma57" + solver.options["halt_on_ampl_error"] = "yes" + solver.options["max_iter"] = 3000 return solver def _solve_doe(self, m, fix=False, opt_option=None): diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index d8ed343352a..f7145ae2a46 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -123,9 +123,9 @@ def result_analysis(self, result=None): if self.prior_FIM is not None: try: fim = fim + self.prior_FIM - self.logger.info('Existed information has been added.') + self.logger.info("Existed information has been added.") except: - raise ValueError('Check the shape of prior FIM.') + raise ValueError("Check the shape of prior FIM.") if np.linalg.cond(fim) > self.max_condition_number: self.logger.info( @@ -133,7 +133,7 @@ def result_analysis(self, result=None): np.linalg.cond(fim), ) self.logger.info( - 'A condition number bigger than %s is considered near singular.', + "A condition number bigger than %s is considered near singular.", self.max_condition_number, ) @@ -239,10 +239,10 @@ def _print_FIM_info(self, FIM): self.eig_vecs = np.linalg.eig(FIM)[1] self.logger.info( - 'FIM: %s; \n Trace: %s; \n Determinant: %s;', self.FIM, self.trace, self.det + "FIM: %s; \n Trace: %s; \n Determinant: %s;", self.FIM, self.trace, self.det ) self.logger.info( - 'Condition number: %s; \n Min eigenvalue: %s.', self.cond, self.min_eig + "Condition number: %s; \n Min eigenvalue: %s.", self.cond, self.min_eig ) def _solution_info(self, m, dv_set): @@ -268,11 +268,11 @@ def _solution_info(self, m, dv_set): # When scaled with constant values, the effect of the scaling factors are removed here # For determinant, the scaling factor to determinant is scaling factor ** (Dim of FIM) # For trace, the scaling factor to trace is the scaling factor. - if self.obj == 'det': + if self.obj == "det": self.obj_det = np.exp(value(m.obj)) / (self.fim_scale_constant_value) ** ( len(self.parameter_names) ) - elif self.obj == 'trace': + elif self.obj == "trace": self.obj_trace = np.exp(value(m.obj)) / (self.fim_scale_constant_value) design_variable_names = list(dv_set.keys()) @@ -314,11 +314,11 @@ def _get_solver_info(self): if (self.result.solver.status == SolverStatus.ok) and ( self.result.solver.termination_condition == TerminationCondition.optimal ): - self.status = 'converged' + self.status = "converged" elif ( self.result.solver.termination_condition == TerminationCondition.infeasible ): - self.status = 'infeasible' + self.status = "infeasible" else: self.status = self.result.solver.status @@ -399,10 +399,10 @@ def extract_criteria(self): column_names.append(i) # Each design criteria has a column to store values - column_names.append('A') - column_names.append('D') - column_names.append('E') - column_names.append('ME') + column_names.append("A") + column_names.append("D") + column_names.append("E") + column_names.append("ME") # generate the dataframe store_all_results = np.asarray(store_all_results) self.store_all_results_dataframe = pd.DataFrame( @@ -458,7 +458,7 @@ def figure_drawing( self.design_names ): raise ValueError( - 'Error: All dimensions except for those the figures are drawn by should be fixed.' + "Error: All dimensions except for those the figures are drawn by should be fixed." ) if len(self.sensitivity_dimension) not in [1, 2]: @@ -467,15 +467,15 @@ def figure_drawing( # generate a combination of logic sentences to filter the results of the DOF needed. # an example filter: (self.store_all_results_dataframe["CA0"]==5). if len(self.fixed_design_names) != 0: - filter = '' + filter = "" for i in range(len(self.fixed_design_names)): - filter += '(self.store_all_results_dataframe[' + filter += "(self.store_all_results_dataframe[" filter += str(self.fixed_design_names[i]) - filter += ']==' + filter += "]==" filter += str(self.fixed_design_values[i]) - filter += ')' + filter += ")" if i != (len(self.fixed_design_names) - 1): - filter += '&' + filter += "&" # extract results with other dimensions fixed figure_result_data = self.store_all_results_dataframe.loc[eval(filter)] # if there is no other fixed dimensions @@ -526,78 +526,78 @@ def _curve1D( # decide if the results are log scaled if log_scale: - y_range_A = np.log10(self.figure_result_data['A'].values.tolist()) - y_range_D = np.log10(self.figure_result_data['D'].values.tolist()) - y_range_E = np.log10(self.figure_result_data['E'].values.tolist()) - y_range_ME = np.log10(self.figure_result_data['ME'].values.tolist()) + y_range_A = np.log10(self.figure_result_data["A"].values.tolist()) + y_range_D = np.log10(self.figure_result_data["D"].values.tolist()) + y_range_E = np.log10(self.figure_result_data["E"].values.tolist()) + y_range_ME = np.log10(self.figure_result_data["ME"].values.tolist()) else: - y_range_A = self.figure_result_data['A'].values.tolist() - y_range_D = self.figure_result_data['D'].values.tolist() - y_range_E = self.figure_result_data['E'].values.tolist() - y_range_ME = self.figure_result_data['ME'].values.tolist() + y_range_A = self.figure_result_data["A"].values.tolist() + y_range_D = self.figure_result_data["D"].values.tolist() + y_range_E = self.figure_result_data["E"].values.tolist() + y_range_ME = self.figure_result_data["ME"].values.tolist() # Draw A-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_A) ax.scatter(x_range, y_range_A) - ax.set_ylabel('$log_{10}$ Trace') + ax.set_ylabel("$log_{10}$ Trace") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': A-optimality') + plt.pyplot.title(title_text + ": A-optimality") plt.pyplot.show() # Draw D-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_D) ax.scatter(x_range, y_range_D) - ax.set_ylabel('$log_{10}$ Determinant') + ax.set_ylabel("$log_{10}$ Determinant") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': D-optimality') + plt.pyplot.title(title_text + ": D-optimality") plt.pyplot.show() # Draw E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_E) ax.scatter(x_range, y_range_E) - ax.set_ylabel('$log_{10}$ Minimal eigenvalue') + ax.set_ylabel("$log_{10}$ Minimal eigenvalue") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': E-optimality') + plt.pyplot.title(title_text + ": E-optimality") plt.pyplot.show() # Draw Modified E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_ME) ax.scatter(x_range, y_range_ME) - ax.set_ylabel('$log_{10}$ Condition number') + ax.set_ylabel("$log_{10}$ Condition number") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': Modified E-optimality') + plt.pyplot.title(title_text + ": Modified E-optimality") plt.pyplot.show() def _heatmap( @@ -641,10 +641,10 @@ def _heatmap( y_range = sensitivity_dict[self.sensitivity_dimension[1]] # extract the design criteria values - A_range = self.figure_result_data['A'].values.tolist() - D_range = self.figure_result_data['D'].values.tolist() - E_range = self.figure_result_data['E'].values.tolist() - ME_range = self.figure_result_data['ME'].values.tolist() + A_range = self.figure_result_data["A"].values.tolist() + D_range = self.figure_result_data["D"].values.tolist() + E_range = self.figure_result_data["E"].values.tolist() + ME_range = self.figure_result_data["ME"].values.tolist() # reshape the design criteria values for heatmaps cri_a = np.asarray(A_range).reshape(len(x_range), len(y_range)) @@ -675,12 +675,12 @@ def _heatmap( # A-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -690,18 +690,18 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_a.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(trace(FIM))') - plt.pyplot.title(title_text + ': A-optimality') + ba.set_label("log10(trace(FIM))") + plt.pyplot.title(title_text + ": A-optimality") plt.pyplot.show() # D-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -711,18 +711,18 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_d.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(det(FIM))') - plt.pyplot.title(title_text + ': D-optimality') + ba.set_label("log10(det(FIM))") + plt.pyplot.title(title_text + ": D-optimality") plt.pyplot.show() # E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -732,18 +732,18 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_e.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(minimal eig(FIM))') - plt.pyplot.title(title_text + ': E-optimality') + ba.set_label("log10(minimal eig(FIM))") + plt.pyplot.title(title_text + ": E-optimality") plt.pyplot.show() # modified E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -753,6 +753,6 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_e2.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(cond(FIM))') - plt.pyplot.title(title_text + ': Modified E-optimality') + ba.set_label("log10(cond(FIM))") + plt.pyplot.title(title_text + ": Modified E-optimality") plt.pyplot.show() diff --git a/pyomo/contrib/doe/scenario.py b/pyomo/contrib/doe/scenario.py index 6c6f5ef7d1b..b44ce1ab4d3 100644 --- a/pyomo/contrib/doe/scenario.py +++ b/pyomo/contrib/doe/scenario.py @@ -150,5 +150,5 @@ def generate_scenario(self): # store scenario if self.store: - with open('scenario_simultaneous.pickle', 'wb') as f: + with open("scenario_simultaneous.pickle", "wb") as f: pickle.dump(self.scenario_data, f) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index b59014a8110..8153e07018a 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -38,7 +38,7 @@ from pyomo.opt import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() class TestReactorExample(unittest.TestCase): diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index d2431f7bd45..05664b0a795 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -40,124 +40,111 @@ class TestMeasurementError(unittest.TestCase): def test_with_time_plus_one_extra_index(self): - """ This tests confirms the typical usage with a time index plus one extra index. + """This tests confirms the typical usage with a time index plus one extra index. This test should execute without throwing any errors. - + """ MeasurementVariables().add_variables( - "C", - indices={0: ['A', 'B', 'C'], 1: [0, 0.5, 1.0]}, - time_index_position=1 + "C", indices={0: ["A", "B", "C"], 1: [0, 0.5, 1.0]}, time_index_position=1 ) def test_with_time_plus_two_extra_indices(self): - """ This tests confirms the typical usage with a time index plus two extra indices. + """This tests confirms the typical usage with a time index plus two extra indices. This test should execute without throwing any errors. - + """ MeasurementVariables().add_variables( "C", - indices={0: ['A', 'B', 'C'], # species - 1: [0, 0.5, 1.0], # time - 2: [1, 2, 3]}, # position - time_index_position=1 + indices={ + 0: ["A", "B", "C"], # species + 1: [0, 0.5, 1.0], # time + 2: [1, 2, 3], + }, # position + time_index_position=1, ) def test_time_index_position_out_of_bounds(self): - """ This test confirms that an error is thrown when the time index position is out of bounds. - - """ + """This test confirms that an error is thrown when the time index position is out of bounds.""" # if time index is not in indices, an value error is thrown. with self.assertRaises(ValueError): MeasurementVariables().add_variables( - "C", - indices={0: ['CA', 'CB', 'CC'], # species - 1: [0, 0.5, 1.0],}, # time - time_index_position=2 # this is out of bounds + "C", + indices={0: ["CA", "CB", "CC"], 1: [0, 0.5, 1.0]}, # species # time + time_index_position=2, # this is out of bounds ) def test_single_measurement_variable(self): - """ This test confirms we can specify a single measurement variable without + """This test confirms we can specify a single measurement variable without specifying the indices. The test should execute with no errors. """ measurements = MeasurementVariables() - measurements.add_variables( - "HelloWorld", - indices=None, - time_index_position=None) + measurements.add_variables("HelloWorld", indices=None, time_index_position=None) def test_without_time_index(self): - """ This test confirms we can add a measurement variable without specifying the time index. + """This test confirms we can add a measurement variable without specifying the time index. The test should execute with no errors. """ MeasurementVariables().add_variables( - "C", - indices= {0: ['CA', 'CB', 'CC']}, # species as only index - time_index_position=None # no time index - ) - + "C", + indices={0: ["CA", "CB", "CC"]}, # species as only index + time_index_position=None, # no time index + ) + def test_only_time_index(self): - """ This test confirms we can add a measurement variable without specifying the variable name. + """This test confirms we can add a measurement variable without specifying the variable name. The test should execute with no errors. """ MeasurementVariables().add_variables( - "HelloWorld", # name of the variable - indices={0: [0, 0.5, 1.0]}, - time_index_position=0 + "HelloWorld", # name of the variable + indices={0: [0, 0.5, 1.0]}, + time_index_position=0, ) def test_with_no_measurement_name(self): - """ This test confirms that an error is thrown when None is used as the measurement name. - - """ + """This test confirms that an error is thrown when None is used as the measurement name.""" with self.assertRaises(TypeError): MeasurementVariables().add_variables( - None, - indices={0: [0, 0.5, 1.0]}, - time_index_position=0 + None, indices={0: [0, 0.5, 1.0]}, time_index_position=0 ) - - def test_with_non_string_measurement_name(self): - """ This test confirms that an error is thrown when a non-string is used as the measurement name. - """ + def test_with_non_string_measurement_name(self): + """This test confirms that an error is thrown when a non-string is used as the measurement name.""" with self.assertRaises(TypeError): MeasurementVariables().add_variables( - 1, - indices={0: [0, 0.5, 1.0]}, - time_index_position=0 + 1, indices={0: [0, 0.5, 1.0]}, time_index_position=0 ) def test_non_integer_index_keys(self): - """ This test confirms that strings can be used as keys for specifying the indices. + """This test confirms that strings can be used as keys for specifying the indices. Warning: it is possible this usage breaks something else in Pyomo.DoE. - There may be an implicit assumption that the order of the keys must match the order + There may be an implicit assumption that the order of the keys must match the order of the indices in the Pyomo model. """ MeasurementVariables().add_variables( "C", - indices={"species": ['CA', 'CB', 'CC'], "time": [0, 0.5, 1.0]}, - time_index_position="time" + indices={"species": ["CA", "CB", "CC"], "time": [0, 0.5, 1.0]}, + time_index_position="time", ) + class TestDesignError(unittest.TestCase): def test(self): t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] @@ -165,7 +152,7 @@ def test(self): exp_design = DesignVariables() # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -201,7 +188,7 @@ def test(self): t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] # measurement object variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ["CA", "CB", "CC"], 1: t_control} measurements = MeasurementVariables() measurements.add_variables( @@ -212,7 +199,7 @@ def test(self): exp_design = DesignVariables() # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] exp_design.add_variables( @@ -225,7 +212,7 @@ def test(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -272,7 +259,7 @@ def test_setup(self): # add variable C variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ["CA", "CB", "CC"], 1: t_control} measurements.add_variables( variable_name, indices=indices, time_index_position=1 ) @@ -285,36 +272,36 @@ def test_setup(self): ) # check variable names - self.assertEqual(measurements.variable_names[0], 'C[CA,0]') - self.assertEqual(measurements.variable_names[1], 'C[CA,0.125]') - self.assertEqual(measurements.variable_names[-1], 'T[5,0.8]') - self.assertEqual(measurements.variable_names[-2], 'T[5,0.6]') - self.assertEqual(measurements.variance['T[5,0.4]'], 10) - self.assertEqual(measurements.variance['T[5,0.6]'], 10) - self.assertEqual(measurements.variance['T[5,0.4]'], 10) - self.assertEqual(measurements.variance['T[5,0.6]'], 10) + self.assertEqual(measurements.variable_names[0], "C[CA,0]") + self.assertEqual(measurements.variable_names[1], "C[CA,0.125]") + self.assertEqual(measurements.variable_names[-1], "T[5,0.8]") + self.assertEqual(measurements.variable_names[-2], "T[5,0.6]") + self.assertEqual(measurements.variance["T[5,0.4]"], 10) + self.assertEqual(measurements.variance["T[5,0.6]"], 10) + self.assertEqual(measurements.variance["T[5,0.4]"], 10) + self.assertEqual(measurements.variance["T[5,0.6]"], 10) ### specify function var_names = [ - 'C[CA,0]', - 'C[CA,0.125]', - 'C[CA,0.875]', - 'C[CA,1]', - 'C[CB,0]', - 'C[CB,0.125]', - 'C[CB,0.25]', - 'C[CB,0.375]', - 'C[CC,0]', - 'C[CC,0.125]', - 'C[CC,0.25]', - 'C[CC,0.375]', + "C[CA,0]", + "C[CA,0.125]", + "C[CA,0.875]", + "C[CA,1]", + "C[CB,0]", + "C[CB,0.125]", + "C[CB,0.25]", + "C[CB,0.375]", + "C[CC,0]", + "C[CC,0.125]", + "C[CC,0.25]", + "C[CC,0.375]", ] measurements2 = MeasurementVariables() measurements2.set_variable_name_list(var_names) - self.assertEqual(measurements2.variable_names[1], 'C[CA,0.125]') - self.assertEqual(measurements2.variable_names[-1], 'C[CC,0.375]') + self.assertEqual(measurements2.variable_names[1], "C[CA,0.125]") + self.assertEqual(measurements2.variable_names[-1], "C[CC,0.375]") ### check_subset function self.assertTrue(measurements.check_subset(measurements2)) @@ -330,7 +317,7 @@ def test_setup(self): exp_design = DesignVariables() # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] exp_design.add_variables( @@ -343,7 +330,7 @@ def test_setup(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -359,31 +346,31 @@ def test_setup(self): self.assertEqual( exp_design.variable_names, [ - 'CA0[0]', - 'T[0]', - 'T[0.125]', - 'T[0.25]', - 'T[0.375]', - 'T[0.5]', - 'T[0.625]', - 'T[0.75]', - 'T[0.875]', - 'T[1]', + "CA0[0]", + "T[0]", + "T[0.125]", + "T[0.25]", + "T[0.375]", + "T[0.5]", + "T[0.625]", + "T[0.75]", + "T[0.875]", + "T[1]", ], ) - self.assertEqual(exp_design.variable_names_value['CA0[0]'], 5) - self.assertEqual(exp_design.variable_names_value['T[0]'], 470) - self.assertEqual(exp_design.upper_bounds['CA0[0]'], 5) - self.assertEqual(exp_design.upper_bounds['T[0]'], 700) - self.assertEqual(exp_design.lower_bounds['CA0[0]'], 1) - self.assertEqual(exp_design.lower_bounds['T[0]'], 300) + self.assertEqual(exp_design.variable_names_value["CA0[0]"], 5) + self.assertEqual(exp_design.variable_names_value["T[0]"], 470) + self.assertEqual(exp_design.upper_bounds["CA0[0]"], 5) + self.assertEqual(exp_design.upper_bounds["T[0]"], 700) + self.assertEqual(exp_design.lower_bounds["CA0[0]"], 1) + self.assertEqual(exp_design.lower_bounds["T[0]"], 300) design_names = exp_design.variable_names exp1 = [4, 600, 300, 300, 300, 300, 300, 300, 300, 300] exp1_design_dict = dict(zip(design_names, exp1)) exp_design.update_values(exp1_design_dict) - self.assertEqual(exp_design.variable_names_value['CA0[0]'], 4) - self.assertEqual(exp_design.variable_names_value['T[0]'], 600) + self.assertEqual(exp_design.variable_names_value["CA0[0]"], 4) + self.assertEqual(exp_design.variable_names_value["T[0]"], 600) class TestParameter(unittest.TestCase): @@ -391,19 +378,19 @@ class TestParameter(unittest.TestCase): def test_setup(self): # set up parameter class - param_dict = {'A1': 84.79, 'A2': 371.72, 'E1': 7.78, 'E2': 15.05} + param_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} scenario_gene = ScenarioGenerator(param_dict, formula="central", step=0.1) parameter_set = scenario_gene.ScenarioData - self.assertAlmostEqual(parameter_set.eps_abs['A1'], 16.9582, places=1) - self.assertAlmostEqual(parameter_set.eps_abs['E1'], 1.5554, places=1) - self.assertEqual(parameter_set.scena_num['A2'], [2, 3]) - self.assertEqual(parameter_set.scena_num['E1'], [4, 5]) - self.assertAlmostEqual(parameter_set.scenario[0]['A1'], 93.2699, places=1) - self.assertAlmostEqual(parameter_set.scenario[2]['A2'], 408.8895, places=1) - self.assertAlmostEqual(parameter_set.scenario[-1]['E2'], 13.54, places=1) - self.assertAlmostEqual(parameter_set.scenario[-2]['E2'], 16.55, places=1) + self.assertAlmostEqual(parameter_set.eps_abs["A1"], 16.9582, places=1) + self.assertAlmostEqual(parameter_set.eps_abs["E1"], 1.5554, places=1) + self.assertEqual(parameter_set.scena_num["A2"], [2, 3]) + self.assertEqual(parameter_set.scena_num["E1"], [4, 5]) + self.assertAlmostEqual(parameter_set.scenario[0]["A1"], 93.2699, places=1) + self.assertAlmostEqual(parameter_set.scenario[2]["A2"], 408.8895, places=1) + self.assertAlmostEqual(parameter_set.scenario[-1]["E2"], 13.54, places=1) + self.assertAlmostEqual(parameter_set.scenario[-2]["E2"], 16.55, places=1) class TestVariablesWithIndices(unittest.TestCase): @@ -414,7 +401,7 @@ def test_setup(self): t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] ### add_element function # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] special.add_variables( @@ -427,7 +414,7 @@ def test_setup(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -443,25 +430,25 @@ def test_setup(self): self.assertEqual( special.variable_names, [ - 'CA0[0]', - 'T[0]', - 'T[0.125]', - 'T[0.25]', - 'T[0.375]', - 'T[0.5]', - 'T[0.625]', - 'T[0.75]', - 'T[0.875]', - 'T[1]', + "CA0[0]", + "T[0]", + "T[0.125]", + "T[0.25]", + "T[0.375]", + "T[0.5]", + "T[0.625]", + "T[0.75]", + "T[0.875]", + "T[1]", ], ) - self.assertEqual(special.variable_names_value['CA0[0]'], 5) - self.assertEqual(special.variable_names_value['T[0]'], 470) - self.assertEqual(special.upper_bounds['CA0[0]'], 5) - self.assertEqual(special.upper_bounds['T[0]'], 700) - self.assertEqual(special.lower_bounds['CA0[0]'], 1) - self.assertEqual(special.lower_bounds['T[0]'], 300) + self.assertEqual(special.variable_names_value["CA0[0]"], 5) + self.assertEqual(special.variable_names_value["T[0]"], 470) + self.assertEqual(special.upper_bounds["CA0[0]"], 5) + self.assertEqual(special.upper_bounds["T[0]"], 700) + self.assertEqual(special.lower_bounds["CA0[0]"], 1) + self.assertEqual(special.lower_bounds["T[0]"], 300) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index daf2ee89194..3fca93b5ded 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -34,7 +34,7 @@ from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure from pyomo.opt import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() class Test_example_options(unittest.TestCase): @@ -70,11 +70,11 @@ def test_setUP(self): # Control time set [h] t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] # Define parameter nominal value - parameter_dict = {'A1': 84.79, 'A2': 371.72, 'E1': 7.78, 'E2': 15.05} + parameter_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} # measurement object variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ["CA", "CB", "CC"], 1: t_control} measurements = MeasurementVariables() measurements.add_variables( @@ -85,7 +85,7 @@ def test_setUP(self): exp_design = DesignVariables() # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] exp_design.add_variables( @@ -98,7 +98,7 @@ def test_setUP(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -216,5 +216,5 @@ def test_setUP(self): self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 1bf27f02b0f94569a98e06cb0c08f7b978630f3c Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:21:35 -0400 Subject: [PATCH 1141/1178] Added reactor_design example for Pyomo.DoE. This is still being debugged. --- pyomo/contrib/doe/examples/reactor_design.py | 174 +++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 pyomo/contrib/doe/examples/reactor_design.py diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py new file mode 100644 index 00000000000..81a64a0a46a --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -0,0 +1,174 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# +# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation +# Initiative (CCSI), and is copyright (c) 2022 by the software owners: +# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., +# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, +# Battelle Memorial Institute, University of Notre Dame, +# The University of Pittsburgh, The University of Texas at Austin, +# University of Toledo, West Virginia University, et al. All rights reserved. +# +# NOTICE. This Software was developed under funding from the +# U.S. Department of Energy and the U.S. Government consequently retains +# certain rights. As such, the U.S. Government has been granted for itself +# and others acting on its behalf a paid-up, nonexclusive, irrevocable, +# worldwide license in the Software to reproduce, distribute copies to the +# public, prepare derivative works, and perform publicly and display +# publicly, and to permit other to do so. +# ___________________________________________________________________________ + +# from pyomo.contrib.parmest.examples.reactor_design import reactor_design_model +# if we refactor to use the same create_model function as parmest, +# we can just import instead of redefining the model + +import pyomo.environ as pyo +from pyomo.dae import ContinuousSet, DerivativeVar +from pyomo.contrib.doe import ModelOptionLib, DesignOfExperiments, MeasurementVariables, DesignVariables + +def create_model( + mod=None, + model_option="stage2"): + + model_option = ModelOptionLib(model_option) + + model = mod + + if model_option == ModelOptionLib.parmest: + model = pyo.ConcreteModel() + return_m = True + elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2: + if model is None: + raise ValueError( + "If model option is stage1 or stage2, a created model needs to be provided." + ) + return_m = False + else: + raise ValueError( + "model_option needs to be defined as parmest, stage1, or stage2." + ) + + # Rate constants + model.k1 = pyo.Var( + initialize=5.0 / 6.0, within=pyo.PositiveReals + ) # min^-1 + model.k2 = pyo.Var( + initialize=5.0 / 3.0, within=pyo.PositiveReals + ) # min^-1 + model.k3 = pyo.Var( + initialize=1.0 / 6000.0, within=pyo.PositiveReals + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + model.caf = pyo.Var(initialize=10000, within=pyo.PositiveReals) + + # Space velocity (flowrate/volume) + model.sv = pyo.Var(initialize=1.0, within=pyo.PositiveReals) + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + if return_m: + return model + +def main(): + + # measurement object + measurements = MeasurementVariables() + measurements.add_variables( + "ca", + indices=None, + time_index_position=None, + ) + measurements.add_variables( + "cb", + indices=None, + time_index_position=None + ) + measurements.add_variables( + "cc", + indices=None, + time_index_position=None + ) + measurements.add_variables( + "cd", + indices=None, + time_index_position=None + ) + + # design object + exp_design = DesignVariables() + exp_design.add_variables( + "sv", + indices=None, + time_index_position=None, + values=1.0, + lower_bounds=0.1, + upper_bounds=10.0 + ) + exp_design.add_variables( + "caf", + indices=None, + time_index_position=None, + values=10000, + lower_bounds=5000, + upper_bounds=15000 + ) + + theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} + + doe1 = DesignOfExperiments( + theta_values, + measurements, + exp_design, + create_model, + prior_FIM=None + ) + + doe1.compute_FIM( + mode="sequential_finite", # calculation mode + scale_nominal_param_value=True, # scale nominal parameter value + formula="central", # formula for finite difference + ) + + doe1.result.result_analysis() + +if __name__ == "__main__": + main() + From 0932c2d308e22eb1875d26f9200d1d36cb6a9a8e Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:28:03 -0400 Subject: [PATCH 1142/1178] Debugged some syntax issues in example --- pyomo/contrib/doe/examples/reactor_design.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 81a64a0a46a..18d6b07c9fb 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -114,7 +114,7 @@ def main(): measurements.add_variables( "ca", indices=None, - time_index_position=None, + time_index_position=None ) measurements.add_variables( "cb", @@ -155,8 +155,8 @@ def main(): doe1 = DesignOfExperiments( theta_values, - measurements, exp_design, + measurements, create_model, prior_FIM=None ) @@ -167,7 +167,7 @@ def main(): formula="central", # formula for finite difference ) - doe1.result.result_analysis() + doe1.result_analysis() if __name__ == "__main__": main() From 0db9100d22b7a8236c9f6bec9dc1775cfb593d75 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:29:09 -0400 Subject: [PATCH 1143/1178] A few more syntax mistakes --- pyomo/contrib/doe/examples/reactor_design.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 18d6b07c9fb..10096f17e69 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -161,13 +161,13 @@ def main(): prior_FIM=None ) - doe1.compute_FIM( + result = doe1.compute_FIM( mode="sequential_finite", # calculation mode scale_nominal_param_value=True, # scale nominal parameter value formula="central", # formula for finite difference ) - doe1.result_analysis() + result.result_analysis() if __name__ == "__main__": main() From ecc4600dcf77a9e234403b85df11e6512122abd2 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:51:11 -0400 Subject: [PATCH 1144/1178] Removed objective from example. --- pyomo/contrib/doe/doe.py | 5 +++++ pyomo/contrib/doe/examples/reactor_design.py | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9356cce360b..0718493a826 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -423,6 +423,11 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # dict for storing model outputs output_record = {} + # add zero (dummy/placeholder) objective function + mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) + + mod.pprint() + # solve model square_result = self._solve_doe(mod, fix=True) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 10096f17e69..273d97c88c5 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -78,9 +78,6 @@ def create_model( model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) - # Objective - model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) - # Constraints model.ca_bal = pyo.Constraint( expr=( From 3fd4784803a86a4aed56ac95a277d2d11a824e78 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 22:01:08 -0400 Subject: [PATCH 1145/1178] Finished adding tests. --- pyomo/contrib/doe/examples/reactor_design.py | 80 ++++++++++---------- pyomo/contrib/doe/tests/test_example.py | 8 ++ 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 273d97c88c5..450a2800ae1 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -26,17 +26,22 @@ # ___________________________________________________________________________ # from pyomo.contrib.parmest.examples.reactor_design import reactor_design_model -# if we refactor to use the same create_model function as parmest, +# if we refactor to use the same create_model function as parmest, # we can just import instead of redefining the model import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar -from pyomo.contrib.doe import ModelOptionLib, DesignOfExperiments, MeasurementVariables, DesignVariables +from pyomo.contrib.doe import ( + ModelOptionLib, + DesignOfExperiments, + MeasurementVariables, + DesignVariables, +) +from pyomo.common.dependencies import numpy as np + + +def create_model(mod=None, model_option="stage2"): -def create_model( - mod=None, - model_option="stage2"): - model_option = ModelOptionLib(model_option) model = mod @@ -54,14 +59,10 @@ def create_model( raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) - + # Rate constants - model.k1 = pyo.Var( - initialize=5.0 / 6.0, within=pyo.PositiveReals - ) # min^-1 - model.k2 = pyo.Var( - initialize=5.0 / 3.0, within=pyo.PositiveReals - ) # min^-1 + model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 + model.k2 = pyo.Var(initialize=5.0 / 3.0, within=pyo.PositiveReals) # min^-1 model.k3 = pyo.Var( initialize=1.0 / 6000.0, within=pyo.PositiveReals ) # m^3/(gmol min) @@ -103,31 +104,16 @@ def create_model( if return_m: return model - + + def main(): # measurement object measurements = MeasurementVariables() - measurements.add_variables( - "ca", - indices=None, - time_index_position=None - ) - measurements.add_variables( - "cb", - indices=None, - time_index_position=None - ) - measurements.add_variables( - "cc", - indices=None, - time_index_position=None - ) - measurements.add_variables( - "cd", - indices=None, - time_index_position=None - ) + measurements.add_variables("ca", indices=None, time_index_position=None) + measurements.add_variables("cb", indices=None, time_index_position=None) + measurements.add_variables("cc", indices=None, time_index_position=None) + measurements.add_variables("cd", indices=None, time_index_position=None) # design object exp_design = DesignVariables() @@ -137,7 +123,7 @@ def main(): time_index_position=None, values=1.0, lower_bounds=0.1, - upper_bounds=10.0 + upper_bounds=10.0, ) exp_design.add_variables( "caf", @@ -145,17 +131,13 @@ def main(): time_index_position=None, values=10000, lower_bounds=5000, - upper_bounds=15000 + upper_bounds=15000, ) theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} doe1 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model, - prior_FIM=None + theta_values, exp_design, measurements, create_model, prior_FIM=None ) result = doe1.compute_FIM( @@ -166,6 +148,20 @@ def main(): result.result_analysis() + # print("log10 Trace of FIM: ", np.log10(result.trace)) + # print("log10 Determinant of FIM: ", np.log10(result.det)) + + # test result + relative_error_trace = abs(np.log10(result.trace) - 6.815) + assert ( + relative_error_trace < 0.01 + ), "log10(tr(FIM)) regression test failed, answer does not match previous result" + + relative_error_det = abs(np.log10(result.det) - 18.719) + assert ( + relative_error_det < 0.01 + ), "log10(det(FIM)) regression test failed, answer does not match previous result" + + if __name__ == "__main__": main() - diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index 8153e07018a..c92725efd3b 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -65,6 +65,14 @@ def test_reactor_grid_search(self): reactor_grid_search.main() + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_design(self): + from pyomo.contrib.doe.examples import reactor_design + + reactor_design.main() + if __name__ == "__main__": unittest.main() From 27df1e5e7b4e4005608efbc2e57c6fb5bec813ea Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 24 May 2024 16:23:35 -0600 Subject: [PATCH 1146/1178] Defer categorizing constraints until after linear presolve --- pyomo/repn/plugins/nl_writer.py | 62 ++++++++++++++++++------------ pyomo/repn/tests/ampl/test_nlv2.py | 56 ++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 0a6f9f3da30..9d66b37a429 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -702,8 +702,7 @@ def write(self, model): objectives.extend(linear_objs) n_objs = len(objectives) - constraints = [] - linear_cons = [] + all_constraints = [] n_ranges = 0 n_equality = 0 n_complementarity_nonlin = 0 @@ -740,22 +739,7 @@ def write(self, model): ub = ub * scale if scale < 0: lb, ub = ub, lb - if expr_info.nonlinear: - constraints.append((con, expr_info, lb, ub)) - elif expr_info.linear: - linear_cons.append((con, expr_info, lb, ub)) - elif not self.config.skip_trivial_constraints: - linear_cons.append((con, expr_info, lb, ub)) - else: # constant constraint and skip_trivial_constraints - c = expr_info.const - if (lb is not None and lb - c > TOL) or ( - ub is not None and ub - c < -TOL - ): - raise InfeasibleConstraintException( - "model contains a trivially infeasible " - f"constraint '{con.name}' (fixed body value " - f"{c} outside bounds [{lb}, {ub}])." - ) + all_constraints.append((con, expr_info, lb, ub)) if linear_presolve: con_id = id(con) if not expr_info.nonlinear and lb == ub and lb is not None: @@ -766,7 +750,7 @@ def write(self, model): # report the last constraint timer.toc('Constraint %s', last_parent, level=logging.DEBUG) else: - timer.toc('Processed %s constraints', len(constraints)) + timer.toc('Processed %s constraints', len(all_constraints)) # This may fetch more bounds than needed, but only in the cases # where variables were completely eliminated while walking the @@ -781,14 +765,44 @@ def write(self, model): del comp_by_linear_var del lcon_by_linear_nnz - # Order the constraints, moving all nonlinear constraints to - # the beginning - n_nonlinear_cons = len(constraints) + # Note: defer categorizing constraints until after presolve, as + # the presolver could result in nonlinear constraints to become + # linear (or trivial) + constraints = [] + linear_cons = [] if eliminated_cons: _removed = eliminated_cons.__contains__ - constraints.extend(filterfalse(lambda c: _removed(id(c[0])), linear_cons)) + _constraints = filterfalse(lambda c: _removed(id(c[0])), all_constraints) else: - constraints.extend(linear_cons) + _constraints = all_constraints + for info in _constraints: + expr_info = info[1] + if expr_info.nonlinear: + if expr_info.nonlinear[1]: + constraints.append(info) + continue + expr_info.const += _evaluate_constant_nl(expr_info.nonlinear[0]) + expr_info.nonlinear = None + if expr_info.linear: + linear_cons.append(info) + elif not self.config.skip_trivial_constraints: + linear_cons.append(info) + else: # constant constraint and skip_trivial_constraints + c = expr_info.const + con, expr_info, lb, ub = info + if (lb is not None and lb - c > TOL) or ( + ub is not None and ub - c < -TOL + ): + raise InfeasibleConstraintException( + "model contains a trivially infeasible " + f"constraint '{con.name}' (fixed body value " + f"{c} outside bounds [{lb}, {ub}])." + ) + + # Order the constraints, moving all nonlinear constraints to + # the beginning + n_nonlinear_cons = len(constraints) + constraints.extend(linear_cons) n_cons = len(constraints) # diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 0aa9fab96f9..09936b45bbc 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1763,7 +1763,11 @@ def test_presolve_zero_coef(self): OUT = io.StringIO() with LoggingIntercept() as LOG: nlinfo = nl_writer.NLWriter().write( - m, OUT, symbolic_solver_labels=True, linear_presolve=True + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + skip_trivial_constraints=False, ) self.assertEqual(LOG.getvalue(), "") @@ -1808,6 +1812,56 @@ def test_presolve_zero_coef(self): k0 #intermediate Jacobian column lengths G0 1 #obj 0 0 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[0][1], LinearExpression([-1.0 * m.z]) + ) + self.assertEqual(nlinfo.eliminated_vars[1], (m.x, 2)) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n2 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #1 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 """, OUT.getvalue(), ) From 374fd7df4577b6eceb068b97c7bce41496a7f3e3 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Fri, 24 May 2024 19:37:04 -0400 Subject: [PATCH 1147/1178] Added logic to support slimmer create_model. I still need to debug one part. --- pyomo/contrib/doe/doe.py | 103 ++++++++++++++----- pyomo/contrib/doe/examples/reactor_design.py | 11 +- 2 files changed, 90 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0718493a826..d8f1781051c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -42,6 +42,7 @@ import inspect +import pyomo.contrib.parmest.utils as utils class CalculationMode(Enum): sequential_finite = "sequential_finite" @@ -115,6 +116,15 @@ def __init__( self.design_vars = design_vars self.create_model = create_model + # check if create model function conforms to the original + # Pyomo.DoE interface + model_option_arg = "model_option" in inspect.getfullargspec(self.create_model).args + mod_arg = "mod" in inspect.getfullargspec(self.create_model).args + if model_option_arg and mod_arg: + self._original_create_model_interface = True + else: + self._original_create_model_interface = False + if args is None: args = {} self.args = args @@ -423,10 +433,17 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # dict for storing model outputs output_record = {} + # Deactivate any existing objective functions + for obj in mod.component_objects(pyo.Objective): + obj.deactivate() + # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) - mod.pprint() + # convert params to vars + # print("self.param.keys():", self.param.keys()) + # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) + # mod.pprint() # solve model square_result = self._solve_doe(mod, fix=True) @@ -495,15 +512,25 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): def _direct_kaug(self): # create model - mod = self.create_model(model_option=ModelOptionLib.parmest, **self.args) + if self._original_create_model_interface: + mod = self.create_model(model_option=ModelOptionLib.parmest, **self.args) + else: + mod = self.create_model(**self.args) # discretize if needed if self.discretize_model is not None: mod = self.discretize_model(mod, block=False) + # Deactivate any existing objective functions + for obj in mod.component_objects(pyo.Objective): + obj.deactivate() + # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) + # convert params to vars + # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) + # set ub and lb to parameters for par in self.param.keys(): cuid = pyo.ComponentUID(par) @@ -608,19 +635,37 @@ def _create_block(self): self.eps_abs = self.scenario_data.eps_abs self.scena_gen = scena_gen - # Create a global model - mod = pyo.ConcreteModel() - - # Set for block/scenarios - mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) - # Determine if create_model takes theta as an optional input pass_theta_to_initialize = ( "theta" in inspect.getfullargspec(self.create_model).args ) # Allow user to self-define complex design variables - self.create_model(mod=mod, model_option=ModelOptionLib.stage1, **self.args) + if self._original_create_model_interface: + + # Create a global model + mod = pyo.ConcreteModel() + + if pass_theta_to_initialize: + # Add model on block with theta values + self.create_model( + mod=mod, + model_option=ModelOptionLib.stage1, + theta=self.param, + **self.args, + ) + else: + # Add model on block without theta values + self.create_model(mod=mod, + model_option=ModelOptionLib.stage1, + **self.args) + + else: + # Create a global model + mod = self.create_model(**self.args) + + # Set for block/scenarios + mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) # Fix parameter values in the copy of the stage1 model (if they exist) for par in self.param: @@ -635,21 +680,33 @@ def block_build(b, s): # create block scenarios # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model - if pass_theta_to_initialize: - # Grab the values of theta for this scenario/block - theta_initialize = self.scenario_data.scenario[s] - # Add model on block with theta values - self.create_model( - mod=b, - model_option=ModelOptionLib.stage2, - theta=theta_initialize, - **self.args, - ) + # TODO: Check if this is correct syntax for adding a model to a block + + if self._original_create_model_interface: + if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block + theta_initialize = self.scenario_data.scenario[s] + # Add model on block with theta values + self.create_model( + mod=b, + model_option=ModelOptionLib.stage2, + theta=theta_initialize, + **self.args, + ) + else: + # Otherwise add model on block without theta values + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, **self.args + ) else: - # Otherwise add model on block without theta values - self.create_model( - mod=b, model_option=ModelOptionLib.stage2, **self.args - ) + # Add model on block + if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block + theta_initialize = self.scenario_data.scenario[s] + # This syntax is not yet correct :( + b = self.create_model(theta=theta_initialize, **self.args) + else: + b = self.create_model(**self.args) # fix parameter values to perturbed values for par in self.param: diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 450a2800ae1..4fbc979041e 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -40,8 +40,10 @@ from pyomo.common.dependencies import numpy as np -def create_model(mod=None, model_option="stage2"): +def create_model(): + # This is the old Pyomo.DoE interface + ''' model_option = ModelOptionLib(model_option) model = mod @@ -59,6 +61,10 @@ def create_model(mod=None, model_option="stage2"): raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) + ''' + + # This is the streamlined Pyomo.DoE interface + model = pyo.ConcreteModel() # Rate constants model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 @@ -102,8 +108,11 @@ def create_model(mod=None, model_option="stage2"): expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) + ''' if return_m: return model + ''' + return model def main(): From ffefdef8217e62b92c0e27f63d20dae50ab923e5 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Fri, 24 May 2024 21:27:01 -0400 Subject: [PATCH 1148/1178] Added and tested support for a "slim" create model interface. This will make the workshop examples much easier. --- pyomo/contrib/doe/doe.py | 21 +++++--- pyomo/contrib/doe/examples/reactor_design.py | 55 +++++++++++++------- pyomo/contrib/doe/tests/test_example.py | 14 +++-- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d8f1781051c..47fee913ba8 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -680,8 +680,6 @@ def block_build(b, s): # create block scenarios # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model - # TODO: Check if this is correct syntax for adding a model to a block - if self._original_create_model_interface: if pass_theta_to_initialize: # Grab the values of theta for this scenario/block @@ -698,22 +696,28 @@ def block_build(b, s): self.create_model( mod=b, model_option=ModelOptionLib.stage2, **self.args ) + + # save block in a temporary variable + mod_ = b else: # Add model on block if pass_theta_to_initialize: # Grab the values of theta for this scenario/block theta_initialize = self.scenario_data.scenario[s] - # This syntax is not yet correct :( - b = self.create_model(theta=theta_initialize, **self.args) + mod_ = self.create_model(theta=theta_initialize, **self.args) else: - b = self.create_model(**self.args) + mod_ = self.create_model(**self.args) # fix parameter values to perturbed values for par in self.param: cuid = pyo.ComponentUID(par) - var = cuid.find_component_on(b) + var = cuid.find_component_on(mod_) var.fix(self.scenario_data.scenario[s][par]) + if not self._original_create_model_interface: + # for the "new"/"slim" interface, we need to add the block to the model + return mod_ + mod.block = pyo.Block(mod.scenario, rule=block_build) # discretize the model @@ -1377,7 +1381,10 @@ def _solve_doe(self, m, fix=False, opt_option=None): # either fix or unfix the design variables mod = self._fix_design( - m, self.design_values, fix_opt=fix, optimize_option=opt_option + m, + self.design_values, + fix_opt=fix, + optimize_option=opt_option ) # if user gives solver, use this solver. if not, use default IPOPT solver diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 4fbc979041e..5c1ecb8a79d 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -40,10 +40,7 @@ from pyomo.common.dependencies import numpy as np -def create_model(): - - # This is the old Pyomo.DoE interface - ''' +def create_model_legacy(mod=None, model_option=None): model_option = ModelOptionLib(model_option) model = mod @@ -61,10 +58,18 @@ def create_model(): raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) - ''' + + model = _create_model_details(model) + + if return_m: + return model + - # This is the streamlined Pyomo.DoE interface +def create_model(): model = pyo.ConcreteModel() + return _create_model_details(model) + +def _create_model_details(model): # Rate constants model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 @@ -108,14 +113,10 @@ def create_model(): expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) - ''' - if return_m: - return model - ''' return model -def main(): +def main(legacy_create_model_interface=False): # measurement object measurements = MeasurementVariables() @@ -145,32 +146,50 @@ def main(): theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} + if legacy_create_model_interface: + create_model_ = create_model_legacy + else: + create_model_ = create_model + doe1 = DesignOfExperiments( - theta_values, exp_design, measurements, create_model, prior_FIM=None + theta_values, + exp_design, + measurements, + create_model_, + prior_FIM=None ) + + result = doe1.compute_FIM( mode="sequential_finite", # calculation mode scale_nominal_param_value=True, # scale nominal parameter value formula="central", # formula for finite difference ) + doe1.model.pprint() + result.result_analysis() + # print("FIM =\n",result.FIM) + # print("jac =\n",result.jaco_information) # print("log10 Trace of FIM: ", np.log10(result.trace)) # print("log10 Determinant of FIM: ", np.log10(result.det)) # test result - relative_error_trace = abs(np.log10(result.trace) - 6.815) + expected_log10_trace = 6.815 + log10_trace = np.log10(result.trace) + relative_error_trace = abs(log10_trace - 6.815) assert ( relative_error_trace < 0.01 - ), "log10(tr(FIM)) regression test failed, answer does not match previous result" + ), "log10(tr(FIM)) regression test failed, answer "+str(round(log10_trace,3))+" does not match expected answer of "+str(expected_log10_trace) - relative_error_det = abs(np.log10(result.det) - 18.719) + expected_log10_det = 18.719 + log10_det = np.log10(result.det) + relative_error_det = abs(log10_det - 18.719) assert ( relative_error_det < 0.01 - ), "log10(det(FIM)) regression test failed, answer does not match previous result" - + ), "log10(det(FIM)) regression test failed, answer "+str(round(log10_det,3))+" does not match expected answer of "+str(expected_log10_det) if __name__ == "__main__": - main() + main(legacy_create_model_interface=False) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index c92725efd3b..d9fb5e39ed4 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -68,11 +68,17 @@ def test_reactor_grid_search(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design(self): + def test_reactor_design_slim_create_model_interface(self): from pyomo.contrib.doe.examples import reactor_design - - reactor_design.main() - + reactor_design.main(legacy_create_model_interface=False) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + + def test_reactor_design_legacy_create_model_interface(self): + from pyomo.contrib.doe.examples import reactor_design + reactor_design.main(legacy_create_model_interface=True) if __name__ == "__main__": unittest.main() From 6b43724ce21480163e3f31589b001cf3c129d9ca Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Fri, 24 May 2024 21:39:41 -0400 Subject: [PATCH 1149/1178] Ran black --- pyomo/contrib/doe/doe.py | 16 ++++----- pyomo/contrib/doe/examples/reactor_design.py | 34 +++++++++++--------- pyomo/contrib/doe/tests/test_example.py | 6 ++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 47fee913ba8..4ae57bb1030 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -44,6 +44,7 @@ import pyomo.contrib.parmest.utils as utils + class CalculationMode(Enum): sequential_finite = "sequential_finite" direct_kaug = "direct_kaug" @@ -118,7 +119,9 @@ def __init__( # check if create model function conforms to the original # Pyomo.DoE interface - model_option_arg = "model_option" in inspect.getfullargspec(self.create_model).args + model_option_arg = ( + "model_option" in inspect.getfullargspec(self.create_model).args + ) mod_arg = "mod" in inspect.getfullargspec(self.create_model).args if model_option_arg and mod_arg: self._original_create_model_interface = True @@ -656,9 +659,9 @@ def _create_block(self): ) else: # Add model on block without theta values - self.create_model(mod=mod, - model_option=ModelOptionLib.stage1, - **self.args) + self.create_model( + mod=mod, model_option=ModelOptionLib.stage1, **self.args + ) else: # Create a global model @@ -1381,10 +1384,7 @@ def _solve_doe(self, m, fix=False, opt_option=None): # either fix or unfix the design variables mod = self._fix_design( - m, - self.design_values, - fix_opt=fix, - optimize_option=opt_option + m, self.design_values, fix_opt=fix, optimize_option=opt_option ) # if user gives solver, use this solver. if not, use default IPOPT solver diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 5c1ecb8a79d..82aa33bb5a9 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -58,17 +58,18 @@ def create_model_legacy(mod=None, model_option=None): raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) - + model = _create_model_details(model) - + if return_m: return model - + def create_model(): model = pyo.ConcreteModel() return _create_model_details(model) + def _create_model_details(model): # Rate constants @@ -152,15 +153,9 @@ def main(legacy_create_model_interface=False): create_model_ = create_model doe1 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model_, - prior_FIM=None + theta_values, exp_design, measurements, create_model_, prior_FIM=None ) - - result = doe1.compute_FIM( mode="sequential_finite", # calculation mode scale_nominal_param_value=True, # scale nominal parameter value @@ -180,16 +175,23 @@ def main(legacy_create_model_interface=False): expected_log10_trace = 6.815 log10_trace = np.log10(result.trace) relative_error_trace = abs(log10_trace - 6.815) - assert ( - relative_error_trace < 0.01 - ), "log10(tr(FIM)) regression test failed, answer "+str(round(log10_trace,3))+" does not match expected answer of "+str(expected_log10_trace) + assert relative_error_trace < 0.01, ( + "log10(tr(FIM)) regression test failed, answer " + + str(round(log10_trace, 3)) + + " does not match expected answer of " + + str(expected_log10_trace) + ) expected_log10_det = 18.719 log10_det = np.log10(result.det) relative_error_det = abs(log10_det - 18.719) - assert ( - relative_error_det < 0.01 - ), "log10(det(FIM)) regression test failed, answer "+str(round(log10_det,3))+" does not match expected answer of "+str(expected_log10_det) + assert relative_error_det < 0.01, ( + "log10(det(FIM)) regression test failed, answer " + + str(round(log10_det, 3)) + + " does not match expected answer of " + + str(expected_log10_det) + ) + if __name__ == "__main__": main(legacy_create_model_interface=False) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index d9fb5e39ed4..635bc3ed82e 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -70,15 +70,17 @@ def test_reactor_grid_search(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_design_slim_create_model_interface(self): from pyomo.contrib.doe.examples import reactor_design + reactor_design.main(legacy_create_model_interface=False) - + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design_legacy_create_model_interface(self): from pyomo.contrib.doe.examples import reactor_design + reactor_design.main(legacy_create_model_interface=True) + if __name__ == "__main__": unittest.main() From 8cbc854580d3bc0e5871396da7943b3669fc2b85 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 19:59:38 -0400 Subject: [PATCH 1150/1178] Added optimization regression test for reactor design example --- pyomo/contrib/doe/examples/reactor_design.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 82aa33bb5a9..3fc5d805c12 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -192,6 +192,53 @@ def main(legacy_create_model_interface=False): + str(expected_log10_det) ) + doe2 = DesignOfExperiments( + theta_values, + exp_design, + measurements, + create_model_, + prior_FIM=None + ) + + square_result2, optimize_result2 = doe2.stochastic_program( + if_optimize=True, + if_Cholesky=True, + scale_nominal_param_value=True, + objective_option="det", + jac_initial=result.jaco_information.copy(), + step = 0.1 + ) + + optimize_result2.result_analysis() + log_det = np.log(optimize_result2.det) + print("log(det) = ",round(log_det,3)) + log_det_expected = 45.199 + assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" + + doe3 = DesignOfExperiments( + theta_values, + exp_design, + measurements, + create_model_, + prior_FIM=None + ) + + square_result3, optimize_result3 = doe3.stochastic_program( + if_optimize=True, + scale_nominal_param_value=True, + objective_option="trace", + jac_initial=result.jaco_information.copy(), + step = 0.1 + ) + + optimize_result3.result_analysis() + log_trace = np.log(optimize_result3.trace) + log_trace_expected = 17.29 + print("log(trace) = ",round(log_trace,3)) + assert abs(log_trace - log_trace_expected) < 0.01, "log(trace) regression test failed" + + + if __name__ == "__main__": main(legacy_create_model_interface=False) From 27ea04185c739511dd04bb78dd823f8517e473f1 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 19:59:58 -0400 Subject: [PATCH 1151/1178] Ran black --- pyomo/contrib/doe/examples/reactor_design.py | 26 +++++++------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 3fc5d805c12..55e23c4a955 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -193,11 +193,7 @@ def main(legacy_create_model_interface=False): ) doe2 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model_, - prior_FIM=None + theta_values, exp_design, measurements, create_model_, prior_FIM=None ) square_result2, optimize_result2 = doe2.stochastic_program( @@ -206,21 +202,17 @@ def main(legacy_create_model_interface=False): scale_nominal_param_value=True, objective_option="det", jac_initial=result.jaco_information.copy(), - step = 0.1 + step=0.1, ) optimize_result2.result_analysis() log_det = np.log(optimize_result2.det) - print("log(det) = ",round(log_det,3)) + print("log(det) = ", round(log_det, 3)) log_det_expected = 45.199 assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" doe3 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model_, - prior_FIM=None + theta_values, exp_design, measurements, create_model_, prior_FIM=None ) square_result3, optimize_result3 = doe3.stochastic_program( @@ -228,17 +220,17 @@ def main(legacy_create_model_interface=False): scale_nominal_param_value=True, objective_option="trace", jac_initial=result.jaco_information.copy(), - step = 0.1 + step=0.1, ) optimize_result3.result_analysis() log_trace = np.log(optimize_result3.trace) log_trace_expected = 17.29 - print("log(trace) = ",round(log_trace,3)) - assert abs(log_trace - log_trace_expected) < 0.01, "log(trace) regression test failed" + print("log(trace) = ", round(log_trace, 3)) + assert ( + abs(log_trace - log_trace_expected) < 0.01 + ), "log(trace) regression test failed" - - if __name__ == "__main__": main(legacy_create_model_interface=False) From 9bed23cc6aaf42f458b6c53423ba1f1743f052c2 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:05:06 -0400 Subject: [PATCH 1152/1178] Easiest implementation of exploiting symmetry. For the reactor design example, this did not change the D-opt iterations but reduced the number needed for A-opt from ~84 to ~54. --- pyomo/contrib/doe/doe.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 4ae57bb1030..9033ed28cf7 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1175,17 +1175,21 @@ def fim_rule(m, p, q): p: parameter q: parameter """ - return ( - m.fim[p, q] - == sum( - 1 - / self.measurement_vars.variance[n] - * m.sensitivity_jacobian[p, n] - * m.sensitivity_jacobian[q, n] - for n in model.measured_variables + + if p > q: + return m.fim[p, q] == m.fim[q, p] + else: + return ( + m.fim[p, q] + == sum( + 1 + / self.measurement_vars.variance[n] + * m.sensitivity_jacobian[p, n] + * m.sensitivity_jacobian[q, n] + for n in model.measured_variables + ) + + m.priorFIM[p, q] * self.fim_scale_constant_value ) - + m.priorFIM[p, q] * self.fim_scale_constant_value - ) model.jacobian_constraint = pyo.Constraint( model.regression_parameters, model.measured_variables, rule=jacobian_rule From bfa597e9b920cf6a7cb9acee5f3650af9fb82b4f Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:39:13 -0400 Subject: [PATCH 1153/1178] Added option to only compute lower elements of FIM. --- pyomo/contrib/doe/doe.py | 19 +++++++++++++++++-- pyomo/contrib/doe/examples/reactor_design.py | 5 +++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9033ed28cf7..752479be269 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -74,6 +74,7 @@ def __init__( discretize_model=None, args=None, logger_level=logging.INFO, + only_compute_fim_lower=True, ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -106,6 +107,8 @@ def __init__( Additional arguments for the create_model function. logger_level: Specify the level of the logger. Change to logging.DEBUG for all messages. + only_compute_fim_lower: + If True, only the lower triangle of the FIM is computed. Default is True. """ # parameters @@ -157,6 +160,8 @@ def __init__( self.logger = logging.getLogger(__name__) self.logger.setLevel(level=logger_level) + self.only_compute_fim_lower = only_compute_fim_lower + def _check_inputs(self): """ Check if the prior FIM is N*N matrix, where N is the number of parameter @@ -342,6 +347,7 @@ def compute_FIM( extract_single_model=None, formula="central", step=0.001, + only_compute_fim_lower=False, ): """ This function calculates the Fisher information matrix (FIM) using sensitivity information obtained @@ -1019,6 +1025,12 @@ def _create_doe_model(self, no_obj=True): ------- model: the DOE model """ + + # Developer recommendation: use the Cholesky decomposition for D-optimality + # The explicit formula is available for benchmarking purposes and is NOT recommended + if self.only_compute_fim_lower and self.objective_option == ObjectiveLib.det and not self.Cholesky_option: + raise ValueError("Cannot compute determinant with explicit formula if only_compute_fim_lower is True.") + model = self._create_block() # variables for jacobian and FIM @@ -1177,7 +1189,10 @@ def fim_rule(m, p, q): """ if p > q: - return m.fim[p, q] == m.fim[q, p] + if self.only_compute_fim_lower: + return pyo.Constraint.Skip + else: + return m.fim[p, q] == m.fim[q, p] else: return ( m.fim[p, q] @@ -1260,7 +1275,7 @@ def trace_calc(m): return m.trace == sum(m.fim[j, j] for j in m.regression_parameters) def det_general(m): - r"""Calculate determinant. Can be applied to FIM of any size. + """Calculate determinant. Can be applied to FIM of any size. det(A) = sum_{\sigma \in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) Use permutation() to get permutations, sgn() to get signature """ diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 55e23c4a955..0fed262d74f 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -162,7 +162,7 @@ def main(legacy_create_model_interface=False): formula="central", # formula for finite difference ) - doe1.model.pprint() + # doe1.model.pprint() result.result_analysis() @@ -208,7 +208,8 @@ def main(legacy_create_model_interface=False): optimize_result2.result_analysis() log_det = np.log(optimize_result2.det) print("log(det) = ", round(log_det, 3)) - log_det_expected = 45.199 + #log_det_expected = 45.199 + log_det_expected = 44.362 assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" doe3 = DesignOfExperiments( From c7c47a92b493ade3ea512a9e53f025c90042415b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:48:15 -0400 Subject: [PATCH 1154/1178] Changed objective scaling from log to log10. This is easier to interpret. --- pyomo/contrib/doe/doe.py | 6 +++--- pyomo/contrib/doe/examples/reactor_design.py | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 752479be269..ef4a7c7c194 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1311,7 +1311,7 @@ def det_general(m): m.regression_parameters, m.regression_parameters, rule=cholesky_imp ) m.Obj = pyo.Objective( - expr=2 * sum(pyo.log(m.L_ele[j, j]) for j in m.regression_parameters), + expr=2 * sum(pyo.log10(m.L_ele[j, j]) for j in m.regression_parameters), sense=pyo.maximize, ) @@ -1319,13 +1319,13 @@ def det_general(m): # if not cholesky but determinant, calculating det and evaluate the OBJ with det m.det = pyo.Var(initialize=np.linalg.det(fim), bounds=(small_number, None)) m.det_rule = pyo.Constraint(rule=det_general) - m.Obj = pyo.Objective(expr=pyo.log(m.det), sense=pyo.maximize) + m.Obj = pyo.Objective(expr=pyo.log10(m.det), sense=pyo.maximize) elif self.objective_option == ObjectiveLib.trace: # if not determinant or cholesky, calculating the OBJ with trace m.trace = pyo.Var(initialize=np.trace(fim), bounds=(small_number, None)) m.trace_rule = pyo.Constraint(rule=trace_calc) - m.Obj = pyo.Objective(expr=pyo.log(m.trace), sense=pyo.maximize) + m.Obj = pyo.Objective(expr=pyo.log10(m.trace), sense=pyo.maximize) # m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) elif self.objective_option == ObjectiveLib.zero: diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 0fed262d74f..67d6ff02fd2 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -206,10 +206,9 @@ def main(legacy_create_model_interface=False): ) optimize_result2.result_analysis() - log_det = np.log(optimize_result2.det) + log_det = np.log10(optimize_result2.det) print("log(det) = ", round(log_det, 3)) - #log_det_expected = 45.199 - log_det_expected = 44.362 + log_det_expected = 19.266 assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" doe3 = DesignOfExperiments( @@ -225,8 +224,8 @@ def main(legacy_create_model_interface=False): ) optimize_result3.result_analysis() - log_trace = np.log(optimize_result3.trace) - log_trace_expected = 17.29 + log_trace = np.log10(optimize_result3.trace) + log_trace_expected = 7.509 print("log(trace) = ", round(log_trace, 3)) assert ( abs(log_trace - log_trace_expected) < 0.01 From 48350655cd1b2e0b15909e976dfd1ddb097ada7d Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:49:15 -0400 Subject: [PATCH 1155/1178] Ran black --- pyomo/contrib/doe/doe.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ef4a7c7c194..d7d7cbd1395 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1028,8 +1028,14 @@ def _create_doe_model(self, no_obj=True): # Developer recommendation: use the Cholesky decomposition for D-optimality # The explicit formula is available for benchmarking purposes and is NOT recommended - if self.only_compute_fim_lower and self.objective_option == ObjectiveLib.det and not self.Cholesky_option: - raise ValueError("Cannot compute determinant with explicit formula if only_compute_fim_lower is True.") + if ( + self.only_compute_fim_lower + and self.objective_option == ObjectiveLib.det + and not self.Cholesky_option + ): + raise ValueError( + "Cannot compute determinant with explicit formula if only_compute_fim_lower is True." + ) model = self._create_block() From 67afe32c30d89c12efadd2922b2af0f4f944821f Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 26 May 2024 15:32:41 -0400 Subject: [PATCH 1156/1178] Divided reaction kinetics unit tests. Changed the assert statements to check the objective instead of the optimal solution. --- pyomo/contrib/doe/tests/test_example.py | 2 +- .../contrib/doe/tests/test_reactor_example.py | 204 +++++++++--------- 2 files changed, 109 insertions(+), 97 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index 635bc3ed82e..e4ffbe89142 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -41,7 +41,7 @@ ipopt_available = SolverFactory("ipopt").available() -class TestReactorExample(unittest.TestCase): +class TestReactorExamples(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not scipy_available, "scipy is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index 3fca93b5ded..d8d28a03d76 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -37,10 +37,9 @@ ipopt_available = SolverFactory("ipopt").available() -class Test_example_options(unittest.TestCase): - """Test the three options in the kinetics example.""" - - def test_setUP(self): +class Test_Reaction_Kinetics_Example(unittest.TestCase): + def test_reaction_kinetics_create_model(self): + """Test the three options in the kinetics example.""" # parmest option mod = create_model(model_option="parmest") @@ -56,16 +55,116 @@ def test_setUP(self): create_model(model_option="stage2") with self.assertRaises(ValueError): - create_model(model_option="NotDefine") + create_model(model_option="NotDefined") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + @unittest.skipIf(not pandas_available, "Pandas is not available") + def test_kinetics_example_sequential_finite_then_optimize(self): + """Test the kinetics example with sequential_finite mode and then optimization""" + doe_object = self.specify_reaction_kinetics() + + # Test FIM calculation at nominal values + sensi_opt = "sequential_finite" + result = doe_object.compute_FIM( + mode=sensi_opt, scale_nominal_param_value=True, formula="central" + ) + result.result_analysis() + self.assertAlmostEqual(np.log10(result.trace), 2.7885, places=2) + self.assertAlmostEqual(np.log10(result.det), 2.8218, places=2) + self.assertAlmostEqual(np.log10(result.min_eig), -1.0123, places=2) + + ### check subset feature + sub_name = "C" + sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} + + measure_subset = MeasurementVariables() + measure_subset.add_variables( + sub_name, indices=sub_indices, time_index_position=1 + ) + sub_result = result.subset(measure_subset) + sub_result.result_analysis() + self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) + self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) + self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) -class Test_doe_object(unittest.TestCase): - """Test the kinetics example with both the sequential_finite mode and the direct_kaug mode""" + ### Test stochastic_program mode + # Prior information (scaled FIM with T=500 and T=300 experiments) + prior = np.asarray( + [ + [28.67892806, 5.41249739, -81.73674601, -24.02377324], + [5.41249739, 26.40935036, -12.41816477, -139.23992532], + [-81.73674601, -12.41816477, 240.46276004, 58.76422806], + [-24.02377324, -139.23992532, 58.76422806, 767.25584508], + ] + ) + doe_object2 = self.specify_reaction_kinetics(prior=prior) + + square_result, optimize_result = doe_object2.stochastic_program( + if_optimize=True, + if_Cholesky=True, + scale_nominal_param_value=True, + objective_option="det", + L_initial=np.linalg.cholesky(prior), + jac_initial=result.jaco_information.copy(), + tee_opt=True, + ) + + optimize_result.result_analysis() + ## 2024-May-26: changing this to test the objective instead of the optimal solution + ## It's possible the objective is flat and the optimal solution is not unique + # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) + # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) + self.assertAlmostEqual(np.log10(optimize_result.det), 5.744, places=2) + + square_result, optimize_result = doe_object2.stochastic_program( + if_optimize=True, + scale_nominal_param_value=True, + objective_option="trace", + jac_initial=result.jaco_information.copy(), + tee_opt=True, + ) + + optimize_result.result_analysis() + ## 2024-May-26: changing this to test the objective instead of the optimal solution + ## It's possible the objective is flat and the optimal solution is not unique + # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) + # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) + self.assertAlmostEqual(np.log10(optimize_result.trace), 3.340, places=2) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @unittest.skipIf(not pandas_available, "Pandas is not available") - def test_setUP(self): + def test_kinetics_example_direct_k_aug(self): + doe_object = self.specify_reaction_kinetics() + + # Test FIM calculation at nominal values + sensi_opt = "direct_kaug" + result = doe_object.compute_FIM( + mode=sensi_opt, scale_nominal_param_value=True, formula="central" + ) + result.result_analysis() + self.assertAlmostEqual(np.log10(result.trace), 2.7211, places=2) + self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) + self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) + + ### check subset feature + sub_name = "C" + sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} + + measure_subset = MeasurementVariables() + measure_subset.add_variables( + sub_name, indices=sub_indices, time_index_position=1 + ) + sub_result = result.subset(measure_subset) + sub_result.result_analysis() + + self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) + self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) + self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) + + def specify_reaction_kinetics(self, prior=None): ### Define inputs # Control time set [h] t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] @@ -111,9 +210,6 @@ def test_setUP(self): upper_bounds=700, ) - ### Test sequential_finite mode - sensi_opt = "sequential_finite" - design_names = exp_design.variable_names exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] exp1_design_dict = dict(zip(design_names, exp1)) @@ -126,94 +222,10 @@ def test_setUP(self): measurements, create_model, discretize_model=disc_for_measure, - ) - - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - - result.result_analysis() - - self.assertAlmostEqual(np.log10(result.trace), 2.7885, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.8218, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.0123, places=2) - - ### check subset feature - sub_name = "C" - sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} - - measure_subset = MeasurementVariables() - measure_subset.add_variables( - sub_name, indices=sub_indices, time_index_position=1 - ) - sub_result = result.subset(measure_subset) - sub_result.result_analysis() - - self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) - self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) - self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) - - ### Test direct_kaug mode - sensi_opt = "direct_kaug" - # Define a new experiment - - exp1 = [5, 570, 400, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - - doe_object = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, - discretize_model=disc_for_measure, - ) - - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - - result.result_analysis() - - self.assertAlmostEqual(np.log10(result.trace), 2.7211, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) - - ### Test stochastic_program mode - - exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - - # add a prior information (scaled FIM with T=500 and T=300 experiments) - prior = np.asarray( - [ - [28.67892806, 5.41249739, -81.73674601, -24.02377324], - [5.41249739, 26.40935036, -12.41816477, -139.23992532], - [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - [-24.02377324, -139.23992532, 58.76422806, 767.25584508], - ] - ) - - doe_object2 = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, prior_FIM=prior, - discretize_model=disc_for_measure, - ) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, - if_Cholesky=True, - scale_nominal_param_value=True, - objective_option="det", - L_initial=np.linalg.cholesky(prior), ) - self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) - self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) + return doe_object if __name__ == "__main__": From 94e8ebfdb28fd1afa15b688d704e5cd7086b3a16 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 09:41:57 -0400 Subject: [PATCH 1157/1178] Fix elements above the diagonal of FIM; this ensures the correct number of degrees of freedom. --- pyomo/contrib/doe/doe.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d7d7cbd1395..7ac754aaa3d 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1182,11 +1182,7 @@ def read_prior(m, i, j): model.regression_parameters, model.regression_parameters, rule=read_prior ) - # TODO: explore exploiting the symmetry of the FIM matrix # The off-diagonal elements are symmetric, thus only half of the elements need to be calculated - # Syntax challenge: determine the order of p and q, i.e., if p > q, then replace with - # equality constraint fim[p, q] == fim[q, p] - def fim_rule(m, p, q): """ m: Pyomo model @@ -1219,6 +1215,15 @@ def fim_rule(m, p, q): model.regression_parameters, model.regression_parameters, rule=fim_rule ) + if self.only_compute_fim_lower: + # Fix the upper half of the FIM matrix elements to be 0.0. + # This eliminates extra variables and ensures the expected number of + # degrees of freedom in the optimization problem. + for p in model.regression_parameters: + for q in model.regression_parameters: + if p > q: + model.fim[p, q].fix(0.0) + return model def _add_objective(self, m): From e80d74f6e72db362bcc3afe56264b1699fc518c5 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 09:45:17 -0400 Subject: [PATCH 1158/1178] Updated test. --- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index d8d28a03d76..aba20c77fa6 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -145,7 +145,7 @@ def test_kinetics_example_direct_k_aug(self): mode=sensi_opt, scale_nominal_param_value=True, formula="central" ) result.result_analysis() - self.assertAlmostEqual(np.log10(result.trace), 2.7211, places=2) + self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) From c6655b677954c598a8dc14aa1d8c3f82ad57b035 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 10:09:29 -0400 Subject: [PATCH 1159/1178] Updated test that uses k_aug. --- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index aba20c77fa6..d00b87d3b5f 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -146,7 +146,7 @@ def test_kinetics_example_direct_k_aug(self): ) result.result_analysis() self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) + self.assertAlmostEqual(np.log10(result.det), 2.8247, places=2) self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) ### check subset feature From c262c5fe8defdac3b4090fca5d4e88722ab4850b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 11:05:02 -0400 Subject: [PATCH 1160/1178] Hopefully, this is the final update to the k_aug test. --- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index d00b87d3b5f..19fb4e61820 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -147,7 +147,7 @@ def test_kinetics_example_direct_k_aug(self): result.result_analysis() self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) self.assertAlmostEqual(np.log10(result.det), 2.8247, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) + self.assertAlmostEqual(np.log10(result.min_eig), -1.0112, places=2) ### check subset feature sub_name = "C" From e8c098e40bfa9cc80cb0eff50f20f2040d06cdc8 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 18:20:32 -0400 Subject: [PATCH 1161/1178] Added error check if there are no measurements. This mistake came up a few times in the tutorial. --- pyomo/contrib/doe/doe.py | 8 ++++++++ pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb | 1 + pyomo/contrib/doe/tests/test_fim_doe.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 7ac754aaa3d..29e45ec4f5c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -139,6 +139,14 @@ def __init__( self.measurement_vars = measurement_vars self.measure_name = self.measurement_vars.variable_names + if ( + self.measurement_vars.variable_names is None + or not self.measurement_vars.variable_names + ): + raise ValueError( + "There are no measurement variables. Check for a modeling mistake." + ) + # check if user-defined solver is given if solver: self.solver = solver diff --git a/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb b/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb index 12d5a610db4..36ec42fbe49 100644 --- a/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb +++ b/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb @@ -87,6 +87,7 @@ "if \"google.colab\" in sys.modules:\n", " !wget \"https://raw.githubusercontent.com/IDAES/idaes-pse/main/scripts/colab_helper.py\"\n", " import colab_helper\n", + "\n", " colab_helper.install_idaes()\n", " colab_helper.install_ipopt()\n", "\n", diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 05664b0a795..d9a8d60fdb4 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -144,6 +144,20 @@ def test_non_integer_index_keys(self): time_index_position="time", ) + def test_no_measurements(self): + """This test confirms that an error is thrown when the user forgets to add any measurements. + + It's okay to have no decision variables. With no measurement variables, the FIM is the zero matrix. + This (no measurements) is a common user mistake. + """ + + with self.assertRaises(ValueError): + decisions = DesignVariables() + measurements = MeasurementVariables() + DesignOfExperiments( + {}, decisions, measurements, create_model, disc_for_measure + ) + class TestDesignError(unittest.TestCase): def test(self): From 4957eea10eeab0c63c61883eaa68023debbd5cc9 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 28 May 2024 08:17:26 -0400 Subject: [PATCH 1162/1178] Removed some LaTeX syntax from doc string. --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 29e45ec4f5c..5c7fe4f6e77 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1295,7 +1295,7 @@ def trace_calc(m): def det_general(m): """Calculate determinant. Can be applied to FIM of any size. - det(A) = sum_{\sigma \in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) + det(A) = sum_{sigma in Sn} (sgn(sigma) * Prod_{i=1}^n a_{i,sigma_i}) Use permutation() to get permutations, sgn() to get signature """ r_list = list(range(len(m.regression_parameters))) From 746737b08dd9d4143e099b07b44ac70b2cf89d15 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 07:51:18 -0600 Subject: [PATCH 1163/1178] Support simplifying/eliminating constant defined variables --- pyomo/repn/plugins/nl_writer.py | 27 ++++++++++++++++++--------- pyomo/repn/tests/ampl/test_nlv2.py | 10 ++++------ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 9d66b37a429..21d64df2a18 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -548,7 +548,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} - self.var_id_to_nl = None + self.var_id_to_nl = {} self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, @@ -1056,8 +1056,8 @@ def write(self, model): row_comments = [f'\t#{lbl}' for lbl in row_labels] col_labels = [labeler(var_map[_id]) for _id in variables] col_comments = [f'\t#{lbl}' for lbl in col_labels] - self.var_id_to_nl = { - _id: f'v{var_idx}{col_comments[var_idx]}' + id2nl = { + _id: f'v{var_idx}{col_comments[var_idx]}\n' for var_idx, _id in enumerate(variables) } # Write out the .row and .col data @@ -1070,10 +1070,12 @@ def write(self, model): else: row_labels = row_comments = [''] * (n_cons + n_objs) col_labels = col_comments = [''] * len(variables) - self.var_id_to_nl = { - _id: f"v{var_idx}" for var_idx, _id in enumerate(variables) - } + id2nl = {_id: f"v{var_idx}\n" for var_idx, _id in enumerate(variables)} + if self.var_id_to_nl: + self.var_id_to_nl.update(id2nl) + else: + self.var_id_to_nl = id2nl _vmap = self.var_id_to_nl if scale_model: template = self.template @@ -1934,7 +1936,7 @@ def _linear_presolve( # nonlinear portion of a defined variable is a constant # expression. This may now be the case if all the variables in # the original nonlinear expression have been fixed. - for expr, info, _ in self.subexpression_cache.values(): + for _id, (expr, info, sub) in self.subexpression_cache.items(): if not info.nonlinear: continue nl, args = info.nonlinear @@ -1949,12 +1951,19 @@ def _linear_presolve( # guarantee that the user actually initialized the # variables. So, we will fall back on parsing the (now # constant) nonlinear fragment and evaluating it. - if info.linear is None: - info.linear = {} info.nonlinear = None info.const += _evaluate_constant_nl( nl % tuple(template.const % eliminated_vars[i].const for i in args) ) + if not info.linear: + # This has resolved to a constant: the ASL will fail for + # defined variables containing ONLY a constant. We + # need to substitute the constant directly into the + # original constraint/objective expression(s) + info.linear = {} + self.used_named_expressions.discard(_id) + self.var_id_to_nl[_id] = self.template.const % info.const + self.subexpression_cache[_id] = (expr, info, [None, None, True]) return eliminated_cons, eliminated_vars diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 09936b45bbc..b33bf6963dc 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2393,16 +2393,14 @@ def test_presolve_fixes_nl_defined_variables(self): 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) 1 0 #nonzeros in Jacobian, obj. gradient 2 1 #max name lengths: constraints, variables - 0 0 0 2 0 #common exprs: b,c,o,c1,o1 -V1 0 1 #nl(e) -n19 -V2 1 1 #e + 0 0 0 1 0 #common exprs: b,c,o,c1,o1 +V1 1 1 #e 0 1 -v1 #nl(e) +n19 C0 #c1 o2 #* n3 -v2 #e +v1 #e x0 #initial guess r #1 ranges (rhs's) 2 0 #c1 From 6aa55f2bf5990eae0549cd1b3cd03cb0e8f102ef Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 07:52:10 -0600 Subject: [PATCH 1164/1178] Simplify handling of NL file newlines around var identifiers --- pyomo/repn/plugins/nl_writer.py | 17 ++++++++++------- pyomo/repn/tests/ampl/test_nlv2.py | 28 ++++++++++++++-------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 21d64df2a18..e542a177247 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1097,9 +1097,7 @@ def write(self, model): ub *= scale var_bounds[_id] = lb, ub # Update _vmap to output scaled variables in NL expressions - _vmap[_id] = ( - template.division + _vmap[_id] + '\n' + template.const % scale - ).rstrip() + _vmap[_id] = template.division + _vmap[_id] + template.const % scale # Update any eliminated variables to point to the (potentially # scaled) substituted variables @@ -1133,7 +1131,7 @@ def write(self, model): "linear subsystem that was removed from the model. " f"Setting '{var_map[_i]}' == {val}" ) - _vmap[_id] = nl.rstrip() % tuple(_vmap[_i] for _i in args) + _vmap[_id] = nl % tuple(_vmap[_i] for _i in args) r_lines = [None] * n_cons for idx, (con, expr_info, lb, ub) in enumerate(constraints): @@ -2020,7 +2018,7 @@ def _write_v_line(self, expr_id, k): lbl = '\t#%s' % info[0].name else: lbl = '' - self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}" + self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}\n" # Do NOT write out 0 coefficients here: doing so fouls up the # ASL's logic for calculating derivatives, leading to 'nan' in # the Hessian results. @@ -2348,7 +2346,9 @@ class text_nl_debug_template(object): less_equal = 'o23\t# le\n' equality = 'o24\t# eq\n' external_fcn = 'f%d %d%s\n' - var = '%s\n' # NOTE: to support scaling, we do NOT include the 'v' here + # NOTE: to support scaling and substitutions, we do NOT include the + # 'v' or the EOL here: + var = '%s' const = 'n%r\n' string = 'h%d:%s\n' monomial = product + const + var.replace('%', '%%') @@ -2392,7 +2392,10 @@ class text_nl_debug_template(object): def _strip_template_comments(vars_, base_): - vars_['unary'] = {k: v[: v.find('\t#')] + '\n' for k, v in base_.unary.items()} + vars_['unary'] = { + k: v[: v.find('\t#')] + '\n' if v[-1] == '\n' else '' + for k, v in base_.unary.items() + } for k, v in base_.__dict__.items(): if type(v) is str and '\t#' in v: v_lines = v.split('\n') diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index b33bf6963dc..9318f1c5d0f 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -98,7 +98,7 @@ def test_divide(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%sn2\n', [id(m.x)])) m.p = 2 @@ -150,7 +150,7 @@ def test_divide(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o2\nn0.5\no5\n%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o2\nn0.5\no5\n%sn2\n', [id(m.x)])) info = INFO() with LoggingIntercept() as LOG: @@ -160,7 +160,7 @@ def test_divide(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o3\no43\n%s\n%s\n', [id(m.x), id(m.x)])) + self.assertEqual(repn.nonlinear, ('o3\no43\n%s%s', [id(m.x), id(m.x)])) def test_errors_divide_by_0(self): m = ConcreteModel() @@ -255,7 +255,7 @@ def test_pow(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%sn2\n', [id(m.x)])) m.p = 1 info = INFO() @@ -542,7 +542,7 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, InvalidNumber(None)) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\n%s\n%s\n%s\n') + self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\n%s%s%s') self.assertEqual(repn.nonlinear[1], [id(m.z[2]), id(m.z[3]), id(m.z[4])]) m.z[3].fix(float('nan')) @@ -592,7 +592,7 @@ def test_eval_pow(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn0.5\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%sn0.5\n', [id(m.x)])) m.x.fix() info = INFO() @@ -617,7 +617,7 @@ def test_eval_abs(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o15\n%s\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o15\n%s', [id(m.x)])) m.x.fix() info = INFO() @@ -642,7 +642,7 @@ def test_eval_unary_func(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o43\n%s\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o43\n%s', [id(m.x)])) m.x.fix() info = INFO() @@ -671,7 +671,7 @@ def test_eval_expr_if_lessEq(self): self.assertEqual(repn.linear, {}) self.assertEqual( repn.nonlinear, - ('o35\no23\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.y)]), + ('o35\no23\n%sn4\no5\n%sn2\n%s', [id(m.x), id(m.x), id(m.y)]), ) m.x.fix() @@ -712,7 +712,7 @@ def test_eval_expr_if_Eq(self): self.assertEqual(repn.linear, {}) self.assertEqual( repn.nonlinear, - ('o35\no24\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.y)]), + ('o35\no24\n%sn4\no5\n%sn2\n%s', [id(m.x), id(m.x), id(m.y)]), ) m.x.fix() @@ -754,7 +754,7 @@ def test_eval_expr_if_ranged(self): self.assertEqual( repn.nonlinear, ( - 'o35\no21\no23\nn1\n%s\no23\n%s\nn4\no5\n%s\nn2\n%s\n', + 'o35\no21\no23\nn1\n%so23\n%sn4\no5\n%sn2\n%s', [id(m.x), id(m.x), id(m.x), id(m.y)], ), ) @@ -815,7 +815,7 @@ class CustomExpression(ScalarExpression): self.assertEqual(len(info.subexpression_cache), 1) obj, repn, info = info.subexpression_cache[id(m.e)] self.assertIs(obj, m.e) - self.assertEqual(repn.nl, ('%s\n', (id(m.e),))) + self.assertEqual(repn.nl, ('%s', (id(m.e),))) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 3) self.assertEqual(repn.linear, {id(m.x): 1}) @@ -842,7 +842,7 @@ def test_nested_operator_zero_arg(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o24\no3\nn1\n%s\nn0\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o24\no3\nn1\n%sn0\n', [id(m.x)])) def test_duplicate_shared_linear_expressions(self): # This tests an issue where AMPLRepn.duplicate() was not copying @@ -929,7 +929,7 @@ def test_AMPLRepn_to_expr(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {id(m.x[2]): 4, id(m.x[3]): 9, id(m.x[4]): 16}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x[2])])) + self.assertEqual(repn.nonlinear, ('o5\n%sn2\n', [id(m.x[2])])) with self.assertRaisesRegex( MouseTrap, "Cannot convert nonlinear AMPLRepn to Pyomo Expression" ): From b7145d9b238df4c64d037e5df9f4d284c8c9d0c9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 07:52:21 -0600 Subject: [PATCH 1165/1178] Add additional testing --- pyomo/repn/tests/ampl/test_nlv2.py | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 9318f1c5d0f..5af34fab149 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2637,3 +2637,67 @@ def test_presolve_fixes_nl_exernal_function(self): OUT.getvalue(), ) ) + + def test_presolve_defined_var_to_const(self): + # This test is derived from a step in an IDAES initiaization + # where the presolver is able to fix enough variables to cause + # the defined variable to be reduced to a constant. We must not + # emit the defined variable (because doing so generates an error + # in the ASL) + m = ConcreteModel() + m.eq = Var(initialize=100) + m.co2 = Var() + m.n2 = Var() + m.E = Expression(expr=60 / (3 * m.co2 - 4 * m.n2 - 5)) + m.con1 = Constraint(expr=m.co2 == 6) + m.con2 = Constraint(expr=m.n2 == 7) + m.con3 = Constraint(expr=8 / m.E == m.eq) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 1 1 0 0 1 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 1 0 #nonzeros in Jacobian, obj. gradient + 4 2 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #con3 +o3 #/ +n8 +n-4 +x1 #initial guess +0 100 #eq +r #1 ranges (rhs's) +4 0 #con3 +b #1 bounds (on variables) +3 #eq +k0 #intermediate Jacobian column lengths +J0 1 #con3 +0 -1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_check_invalid_monomial_constraints(self): + # This checks issue #3272 + m = ConcreteModel() + m.x = Var() + m.c = Constraint(expr=m.x == 5) + m.d = Constraint(expr=m.x >= 10) + + OUT = io.StringIO() + with self.assertRaisesRegex( + nl_writer.InfeasibleConstraintException, + r"model contains a trivially infeasible constraint 'd' " + r"\(fixed body value 5.0 outside bounds \[10, None\]\)\.", + ): + nl_writer.NLWriter().write(m, OUT, linear_presolve=True) From 4d10cdfc91f19a747ac3b2d7e6c830242de7f95d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 08:43:16 -0600 Subject: [PATCH 1166/1178] NFC: fix spelling --- pyomo/repn/tests/ampl/test_nlv2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 5af34fab149..01df5a93257 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2527,7 +2527,7 @@ def test_presolve_fixes_nl_defined_variables(self): ) ) - def test_presolve_fixes_nl_exernal_function(self): + def test_presolve_fixes_nl_external_function(self): # This tests a workaround for a bug in the ASL where external # functions with constant argument expressions are not # evaluated correctly. From cd62d5bdd12836ebd7dcf2f8301d88bc1233f2fa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 02:10:20 -0600 Subject: [PATCH 1167/1178] Improve resolution of constant external function / defined variable subexpressions --- pyomo/repn/plugins/nl_writer.py | 106 ++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e542a177247..3c90cf7687f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -548,7 +548,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} - self.var_id_to_nl = {} + self.var_id_to_nl_map = {} self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, @@ -622,6 +622,7 @@ def write(self, model): ostream = self.ostream linear_presolve = self.config.linear_presolve + nl_map = self.var_id_to_nl_map var_map = self.var_map initialize_var_map_from_column_order(model, self.config, var_map) timer.toc('Initialized column order', level=logging.DEBUG) @@ -752,6 +753,12 @@ def write(self, model): else: timer.toc('Processed %s constraints', len(all_constraints)) + # We have identified all the external functions (resolving them + # by name). Now we may need to resolve the function by the + # (local) FID, which we know is indexed by integers starting at + # 0. We will convert the dict to a list for efficient lookup. + self.external_functions = list(self.external_functions.values()) + # This may fetch more bounds than needed, but only in the cases # where variables were completely eliminated while walking the # expressions, or when users provide superfluous variables in @@ -766,7 +773,7 @@ def write(self, model): del lcon_by_linear_nnz # Note: defer categorizing constraints until after presolve, as - # the presolver could result in nonlinear constraints to become + # the presolver could result in nonlinear constraints becoming # linear (or trivial) constraints = [] linear_cons = [] @@ -778,10 +785,13 @@ def write(self, model): for info in _constraints: expr_info = info[1] if expr_info.nonlinear: - if expr_info.nonlinear[1]: + nl, args = expr_info.nonlinear + if any(vid not in nl_map for vid in args): constraints.append(info) continue - expr_info.const += _evaluate_constant_nl(expr_info.nonlinear[0]) + expr_info.const += _evaluate_constant_nl( + nl % tuple(nl_map[i] for i in args), self.external_functions + ) expr_info.nonlinear = None if expr_info.linear: linear_cons.append(info) @@ -1072,11 +1082,10 @@ def write(self, model): col_labels = col_comments = [''] * len(variables) id2nl = {_id: f"v{var_idx}\n" for var_idx, _id in enumerate(variables)} - if self.var_id_to_nl: - self.var_id_to_nl.update(id2nl) + if nl_map: + nl_map.update(id2nl) else: - self.var_id_to_nl = id2nl - _vmap = self.var_id_to_nl + self.var_id_to_nl_map = nl_map = id2nl if scale_model: template = self.template objective_scaling = [scaling_cache[id(info[0])] for info in objectives] @@ -1096,8 +1105,8 @@ def write(self, model): if ub is not None: ub *= scale var_bounds[_id] = lb, ub - # Update _vmap to output scaled variables in NL expressions - _vmap[_id] = template.division + _vmap[_id] + template.const % scale + # Update nl_map to output scaled variables in NL expressions + nl_map[_id] = template.division + nl_map[_id] + template.const % scale # Update any eliminated variables to point to the (potentially # scaled) substituted variables @@ -1106,7 +1115,7 @@ def write(self, model): for _i in args: # It is possible that the eliminated variable could # reference another variable that is no longer part of - # the model and therefore does not have a _vmap entry. + # the model and therefore does not have a nl_map entry. # This can happen when there is an underdetermined # independent linear subsystem and the presolve removed # all the constraints from the subsystem. Because the @@ -1114,7 +1123,7 @@ def write(self, model): # anywhere else in the model, they are not part of the # `variables` list. Implicitly "fix" it to an arbitrary # valid value from the presolved domain (see #3192). - if _i not in _vmap: + if _i not in nl_map: lb, ub = var_bounds[_i] if lb is None: lb = -inf @@ -1125,13 +1134,13 @@ def write(self, model): else: val = lb if abs(lb) < abs(ub) else ub eliminated_vars[_i] = AMPLRepn(val, {}, None) - _vmap[_i] = expr_info.compile_repn(visitor)[0] + nl_map[_i] = expr_info.compile_repn(visitor)[0] logger.warning( "presolve identified an underdetermined independent " "linear subsystem that was removed from the model. " f"Setting '{var_map[_i]}' == {val}" ) - _vmap[_id] = nl % tuple(_vmap[_i] for _i in args) + nl_map[_id] = nl % tuple(nl_map[_i] for _i in args) r_lines = [None] * n_cons for idx, (con, expr_info, lb, ub) in enumerate(constraints): @@ -1330,7 +1339,7 @@ def write(self, model): # "F" lines (external function definitions) # amplfunc_libraries = set() - for fid, fcn in sorted(self.external_functions.values()): + for fid, fcn in self.external_functions: amplfunc_libraries.add(fcn._library) ostream.write("F%d 1 -1 %s\n" % (fid, fcn._function)) @@ -1774,6 +1783,7 @@ def _linear_presolve( var_map = self.var_map substitutions_by_linear_var = defaultdict(set) template = self.template + nl_map = self.var_id_to_nl_map one_var = lcon_by_linear_nnz[1] two_var = lcon_by_linear_nnz[2] while 1: @@ -1783,6 +1793,7 @@ def _linear_presolve( b, _ = var_bounds[_id] logger.debug("NL presolve: bounds fixed %s := %s", var_map[_id], b) eliminated_vars[_id] = AMPLRepn(b, {}, None) + nl_map[_id] = template.const % b elif one_var: con_id, info = one_var.popitem() expr_info, lb = info @@ -1792,6 +1803,7 @@ def _linear_presolve( b = expr_info.const = (lb - expr_info.const) / coef logger.debug("NL presolve: substituting %s := %s", var_map[_id], b) eliminated_vars[_id] = expr_info + nl_map[_id] = template.const % b lb, ub = var_bounds[_id] if (lb is not None and lb - b > TOL) or ( ub is not None and ub - b < -TOL @@ -1929,30 +1941,31 @@ def _linear_presolve( expr_info.linear[x] += c * a elif a: expr_info.linear[x] = c * a + elif not expr_info.linear: + nl_map[resubst] = template.const % expr_info.const # Note: the ASL will (silently) produce incorrect answers if the # nonlinear portion of a defined variable is a constant # expression. This may now be the case if all the variables in # the original nonlinear expression have been fixed. for _id, (expr, info, sub) in self.subexpression_cache.items(): - if not info.nonlinear: - continue - nl, args = info.nonlinear - if not args or any( - vid not in eliminated_vars or eliminated_vars[vid].linear - for vid in args - ): - continue - # Ideally, we would just evaluate the named expression. - # However, there might be a linear portion of the named - # expression that still has free variables, and there is no - # guarantee that the user actually initialized the - # variables. So, we will fall back on parsing the (now - # constant) nonlinear fragment and evaluating it. - info.nonlinear = None - info.const += _evaluate_constant_nl( - nl % tuple(template.const % eliminated_vars[i].const for i in args) - ) + if info.nonlinear: + nl, args = info.nonlinear + # Note: 'not args' skips string arguments + # Note: 'vid in nl_map' skips eliminated + # variables and defined variables reduced to constants + if not args or any(vid not in nl_map for vid in args): + continue + # Ideally, we would just evaluate the named expression. + # However, there might be a linear portion of the named + # expression that still has free variables, and there is no + # guarantee that the user actually initialized the + # variables. So, we will fall back on parsing the (now + # constant) nonlinear fragment and evaluating it. + info.nonlinear = None + info.const += _evaluate_constant_nl( + nl % tuple(nl_map[i] for i in args), self.external_functions + ) if not info.linear: # This has resolved to a constant: the ASL will fail for # defined variables containing ONLY a constant. We @@ -1960,7 +1973,7 @@ def _linear_presolve( # original constraint/objective expression(s) info.linear = {} self.used_named_expressions.discard(_id) - self.var_id_to_nl[_id] = self.template.const % info.const + nl_map[_id] = template.const % info.const self.subexpression_cache[_id] = (expr, info, [None, None, True]) return eliminated_cons, eliminated_vars @@ -1990,18 +2003,20 @@ def _write_nl_expression(self, repn, include_const): # constant as the second argument, so we will too. nl = self.template.binary_sum + nl + self.template.const % repn.const try: - self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) + self.ostream.write( + nl % tuple(map(self.var_id_to_nl_map.__getitem__, args)) + ) except KeyError: final_args = [] for arg in args: - if arg in self.var_id_to_nl: - final_args.append(self.var_id_to_nl[arg]) + if arg in self.var_id_to_nl_map: + final_args.append(self.var_id_to_nl_map[arg]) else: _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( self.visitor ) final_args.append( - _nl % tuple(map(self.var_id_to_nl.__getitem__, _ids)) + _nl % tuple(map(self.var_id_to_nl_map.__getitem__, _ids)) ) self.ostream.write(nl % tuple(final_args)) @@ -2018,7 +2033,7 @@ def _write_v_line(self, expr_id, k): lbl = '\t#%s' % info[0].name else: lbl = '' - self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}\n" + self.var_id_to_nl_map[expr_id] = f"v{self.next_V_line_id}{lbl}\n" # Do NOT write out 0 coefficients here: doing so fouls up the # ASL's logic for calculating derivatives, leading to 'nan' in # the Hessian results. @@ -3167,7 +3182,7 @@ def finalizeResult(self, result): return ans -def _evaluate_constant_nl(nl): +def _evaluate_constant_nl(nl, external_functions): expr = nl.splitlines() stack = [] while expr: @@ -3182,6 +3197,15 @@ def _evaluate_constant_nl(nl): # skip blank lines if not tokens: continue + if tokens[0][0] == 'f': + # external function + fid, nargs = tokens + fid = int(fid[1:]) + nargs = int(nargs) + fcn_id, ef = external_functions[fid] + assert fid == fcn_id + stack.append(ef.evaluate(tuple(stack.pop() for i in range(nargs)))) + continue raise DeveloperError( f"Unsupported line format _evaluate_constant_nl() " f"(we expect each line to contain a single token): '{line}'" @@ -3205,6 +3229,8 @@ def _evaluate_constant_nl(nl): # sum) or a string argument. Preserve it as-is until later # when we know which we are expecting. stack.append(term) + elif cmd == 'h': + stack.append(term.split(':', 1)[1]) else: raise DeveloperError( f"Unsupported NL operator in _evaluate_constant_nl(): '{line}'" From 5d4ce0808ab96e8003e14983fc01f80edc9271dd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 02:10:43 -0600 Subject: [PATCH 1168/1178] bugfix: undefined variable --- pyomo/repn/plugins/nl_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3c90cf7687f..8a41eb568e7 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2779,8 +2779,8 @@ def handle_external_function_node(visitor, node, *args): "correctly." % ( func, - visitor.external_byFcn[func]._library, - visitor.external_byFcn[func]._library.name, + visitor.external_functions[func]._library, + visitor.external_functions[func]._library.name, node._fcn._library, node._fcn.name, ) From 2f0e57a497c9e8e9e2f1ca739d114d41cfd4115f Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Wed, 29 May 2024 09:29:34 -0400 Subject: [PATCH 1169/1178] Addressed feedback, ran black. --- pyomo/contrib/doe/doe.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 5c7fe4f6e77..a120add4200 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -42,7 +42,7 @@ import inspect -import pyomo.contrib.parmest.utils as utils +from pyomo.common import DeveloperError class CalculationMode(Enum): @@ -457,11 +457,6 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) - # convert params to vars - # print("self.param.keys():", self.param.keys()) - # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) - # mod.pprint() - # solve model square_result = self._solve_doe(mod, fix=True) @@ -545,9 +540,6 @@ def _direct_kaug(self): # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) - # convert params to vars - # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) - # set ub and lb to parameters for par in self.param.keys(): cuid = pyo.ComponentUID(par) @@ -1294,8 +1286,8 @@ def trace_calc(m): return m.trace == sum(m.fim[j, j] for j in m.regression_parameters) def det_general(m): - """Calculate determinant. Can be applied to FIM of any size. - det(A) = sum_{sigma in Sn} (sgn(sigma) * Prod_{i=1}^n a_{i,sigma_i}) + r"""Calculate determinant. Can be applied to FIM of any size. + det(A) = \sum_{\sigma in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) Use permutation() to get permutations, sgn() to get signature """ r_list = list(range(len(m.regression_parameters))) From 15088dee1f4b05e7225c5fb9cf3a58931ef1e79b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 07:38:45 -0600 Subject: [PATCH 1170/1178] Updating baseline to reflect improved linear constraint detection --- pyomo/repn/tests/ampl/test_nlv2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 01df5a93257..3ca41e97ba4 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2657,11 +2657,15 @@ def test_presolve_defined_var_to_const(self): nl_writer.NLWriter().write( m, OUT, symbolic_solver_labels=True, linear_presolve=True ) + # Note that the presolve will end up recognizing con3 as a + # linear constraint; however, it does not do so until processing + # the constraints after presolve (so the constraint is not + # actually removed and the eq variable still appears in the model) self.assertEqual( *nl_diff( """g3 1 1 0 #problem unknown 1 1 0 0 1 #vars, constraints, objectives, ranges, eqns - 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb 0 0 #network constraints: nonlinear, linear 0 0 0 #nonlinear vars in constraints, objectives, both 0 0 0 1 #linear network variables; functions; arith, flags @@ -2670,13 +2674,11 @@ def test_presolve_defined_var_to_const(self): 4 2 #max name lengths: constraints, variables 0 0 0 0 0 #common exprs: b,c,o,c1,o1 C0 #con3 -o3 #/ -n8 -n-4 +n0 x1 #initial guess 0 100 #eq r #1 ranges (rhs's) -4 0 #con3 +4 2.0 #con3 b #1 bounds (on variables) 3 #eq k0 #intermediate Jacobian column lengths From 1bed2c601ec8ee515892e4981a684cec7a0a7fd0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 11:27:57 -0600 Subject: [PATCH 1171/1178] NFC: fix typos --- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/tests/ampl/test_nlv2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8a41eb568e7..43fd2fade68 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1946,7 +1946,7 @@ def _linear_presolve( # Note: the ASL will (silently) produce incorrect answers if the # nonlinear portion of a defined variable is a constant - # expression. This may now be the case if all the variables in + # expression. This may not be the case if all the variables in # the original nonlinear expression have been fixed. for _id, (expr, info, sub) in self.subexpression_cache.items(): if info.nonlinear: diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 01df5a93257..7d3b499a4ae 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2639,7 +2639,7 @@ def test_presolve_fixes_nl_external_function(self): ) def test_presolve_defined_var_to_const(self): - # This test is derived from a step in an IDAES initiaization + # This test is derived from a step in an IDAES initialization # where the presolver is able to fix enough variables to cause # the defined variable to be reduced to a constant. We must not # emit the defined variable (because doing so generates an error From c212c13a4ab468ba61dd88c29322f889ca9ee150 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 13:29:15 -0600 Subject: [PATCH 1172/1178] Updating CHANGELOG in preparation for the 6.7.3 release --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b4ecbf785..b39165297f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.3 (29 May 2024) +------------------------------------------------------------------------------- + +- Core + - Deprecate `pyomo.core.plugins.transform.model.to_standard_form()` (#3265) + - Reorder definitions to avoid `NameError` in some situations (#3264) +- Testing + - Add URL checking to GHA linting job (#3259, #3261) + - Skip Windows Python 3.8 conda GHA job (#3269) +- Contributed Packages + - DoE: Bug fixes for workshop (#3267) + ------------------------------------------------------------------------------- Pyomo 6.7.2 (9 May 2024) ------------------------------------------------------------------------------- @@ -57,7 +70,7 @@ Pyomo 6.7.2 (9 May 2024) - CP: Add SequenceVar and other logical expressions for scheduling (#3227) - DoE: Bug fixes (#3245) - iis: Add minimal intractable system infeasibility diagnostics (#3172) - - incidence_analysis: Improve `solve_strongly_connected_components` + - incidence_analysis: Improve `solve_strongly_connected_components` performance for models with named expressions (#3186) - incidence_analysis: Add function to plot incidence graph in Dulmage-Mendelsohn order (#3207) From 97a6ae08aa967a7aaa6fe9ef6e42bafe1eb29eca Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 14:37:14 -0600 Subject: [PATCH 1173/1178] More edits to the CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39165297f3..f9051e80ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Pyomo 6.7.3 (29 May 2024) - Core - Deprecate `pyomo.core.plugins.transform.model.to_standard_form()` (#3265) - Reorder definitions to avoid `NameError` in some situations (#3264) +- Solver Interfaces + - NLv2: Fix linear presolver with constant defined vars/external fcns (#3276) - Testing - Add URL checking to GHA linting job (#3259, #3261) - Skip Windows Python 3.8 conda GHA job (#3269) From ba2d2cac411e0000bab03eb48b16bb0d573c2fed Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 14:42:45 -0600 Subject: [PATCH 1174/1178] Updating deprecation version to 6.7.3 --- pyomo/core/plugins/transform/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 9f370c96304..8fe828854ce 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -24,7 +24,7 @@ @deprecated( "to_standard_form() is deprecated. " "Please use WriterFactory('compile_standard_form')", - version='6.7.3.dev0', + version='6.7.3', remove_in='6.8.0', ) def to_standard_form(self): From 32e6431e876b90f5268ca448581c50124d7adf68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 14:54:47 -0600 Subject: [PATCH 1175/1178] Update guard for pint import to possibly avoid recursion error on FreeBSD --- pyomo/contrib/viewer/tests/test_data_model_item.py | 9 ++------- pyomo/contrib/viewer/tests/test_data_model_tree.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/viewer/tests/test_data_model_item.py b/pyomo/contrib/viewer/tests/test_data_model_item.py index 781ca25508a..d780b315044 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_item.py +++ b/pyomo/contrib/viewer/tests/test_data_model_item.py @@ -46,15 +46,10 @@ from pyomo.contrib.viewer.model_browser import ComponentDataItem from pyomo.contrib.viewer.ui_data import UIData from pyomo.common.dependencies import DeferredImportError +from pyomo.core.base.units_container import pint_available -try: - x = pyo.units.m - units_available = True -except DeferredImportError: - units_available = False - -@unittest.skipIf(not units_available, "Pyomo units are not available") +@unittest.skipIf(not pint_available, "Pyomo units are not available") class TestDataModelItem(unittest.TestCase): def setUp(self): # Borrowed this test model from the trust region tests diff --git a/pyomo/contrib/viewer/tests/test_data_model_tree.py b/pyomo/contrib/viewer/tests/test_data_model_tree.py index d517c91b353..2e5c3592198 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_tree.py +++ b/pyomo/contrib/viewer/tests/test_data_model_tree.py @@ -42,12 +42,7 @@ from pyomo.contrib.viewer.model_browser import ComponentDataModel import pyomo.contrib.viewer.qt as myqt from pyomo.common.dependencies import DeferredImportError - -try: - _x = pyo.units.m - units_available = True -except DeferredImportError: - units_available = False +from pyomo.core.base.units_container import pint_available available = myqt.available @@ -63,7 +58,7 @@ def __init__(*args, **kwargs): pass -@unittest.skipIf(not available or not units_available, "PyQt or units not available") +@unittest.skipIf(not available or not pint_available, "PyQt or units not available") class TestDataModel(unittest.TestCase): def setUp(self): # Borrowed this test model from the trust region tests From f12825eb98f96807cecbfc9515f41b2b8c0e9b16 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 15:14:28 -0600 Subject: [PATCH 1176/1178] More edits to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9051e80ade..8d1d1e45e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Pyomo 6.7.3 (29 May 2024) - Skip Windows Python 3.8 conda GHA job (#3269) - Contributed Packages - DoE: Bug fixes for workshop (#3267) + - viewer: Update guard for pint import (#3277) ------------------------------------------------------------------------------- Pyomo 6.7.2 (9 May 2024) From 93e5dab925115d564af09f8a33ff4099b0247083 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 15:23:21 -0600 Subject: [PATCH 1177/1178] Finalizing 6.7.3 release files --- .coin-or/projDesc.xml | 4 ++-- RELEASE.md | 2 +- pyomo/version/info.py | 4 ++-- setup.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index 073efd968a7..d13ac8804cf 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.2 - 6.7.2 + 6.7.3 + 6.7.3 diff --git a/RELEASE.md b/RELEASE.md index b0228e53944..e42469cbad5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -We are pleased to announce the release of Pyomo 6.7.2. +We are pleased to announce the release of Pyomo 6.7.3. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 36945e8e011..cba680d50ee 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -27,8 +27,8 @@ major = 6 minor = 7 micro = 3 -releaselevel = 'invalid' -# releaselevel = 'final' +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': diff --git a/setup.py b/setup.py index 6d28e4d184b..9dfa253815e 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy', # Needed by autodoc for pynumero + 'numpy<2.0.0', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -273,7 +273,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy', + 'numpy<2.0.0', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 5c3af84e27c19b10987ab451ab892496f5bb9630 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 16:03:44 -0600 Subject: [PATCH 1178/1178] Resetting main for development (6.7.4.dev0) --- pyomo/version/info.py | 6 +++--- setup.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index cba680d50ee..825483a70a0 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 3 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 4 +releaselevel = 'invalid' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': diff --git a/setup.py b/setup.py index 9dfa253815e..6d28e4d184b 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy<2.0.0', # Needed by autodoc for pynumero + 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -273,7 +273,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy<2.0.0', + 'numpy', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units