1import abc
2import enum
3from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple
4from pyomo.core.base.constraint import _GeneralConstraintData, Constraint
5from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint
6from pyomo.core.base.var import _GeneralVarData, Var
7from pyomo.core.base.param import _ParamData, Param
8from pyomo.core.base.block import _BlockData, Block
9from pyomo.core.base.objective import _GeneralObjectiveData
10from pyomo.core.base.suffix import Suffix
11from pyomo.common.collections import ComponentMap, OrderedSet
12from .utils.get_objective import get_objective
13from .utils.identify_named_expressions import identify_named_expressions
14from pyomo.common.timing import HierarchicalTimer
15from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat
16from pyomo.common.errors import ApplicationError
17from pyomo.opt.base import SolverFactory as LegacySolverFactory
18from pyomo.common.factory import Factory
19import logging
20import os
21from pyomo.opt.results.results_ import SolverResults as LegacySolverResults
22from pyomo.opt.results.solution import Solution as LegacySolution, SolutionStatus as LegacySolutionStatus
23from pyomo.opt.results.solver import TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus
24from pyomo.core.kernel.objective import minimize, maximize
25from pyomo.core.base import SymbolMap
26import weakref
27from io import StringIO
28
29
30class TerminationCondition(enum.Enum):
31    """
32    An enumeration for checking the termination condition of solvers
33    """
34    unknown = 0
35    """unknown serves as both a default value, and it is used when no other enum member makes sense"""
36
37    maxTimeLimit = 1
38    """The solver exited due to a time limit"""
39
40    maxIterations = 2
41    """The solver exited due to an iteration limit """
42
43    objectiveLimit = 3
44    """The solver exited due to an objective limit"""
45
46    minStepLength = 4
47    """The solver exited due to a minimum step length"""
48
49    optimal = 5
50    """The solver exited with the optimal solution"""
51
52    unbounded = 8
53    """The solver exited because the problem is unbounded"""
54
55    infeasible = 9
56    """The solver exited because the problem is infeasible"""
57
58    infeasibleOrUnbounded = 10
59    """The solver exited because the problem is either infeasible or unbounded"""
60
61    error = 11
62    """The solver exited due to an error"""
63
64    interrupted = 12
65    """The solver exited because it was interrupted"""
66
67    licensingProblems = 13
68    """The solver exited due to licensing problems"""
69
70
71class SolverConfig(ConfigDict):
72    """
73    Attributes
74    ----------
75    time_limit: float
76        Time limit for the solver
77    stream_solver: bool
78        If True, then the solver log goes to stdout
79    load_solution: bool
80        If False, then the values of the primal variables will not be
81        loaded into the model
82    symbolic_solver_labels: bool
83        If True, the names given to the solver will reflect the names
84        of the pyomo components. Cannot be changed after set_instance
85        is called.
86    report_timing: bool
87        If True, then some timing information will be printed at the
88        end of the solve.
89    """
90    def __init__(self,
91                 description=None,
92                 doc=None,
93                 implicit=False,
94                 implicit_domain=None,
95                 visibility=0):
96        super(SolverConfig, self).__init__(description=description,
97                                           doc=doc,
98                                           implicit=implicit,
99                                           implicit_domain=implicit_domain,
100                                           visibility=visibility)
101
102        self.declare('time_limit', ConfigValue(domain=NonNegativeFloat))
103        self.declare('stream_solver', ConfigValue(domain=bool))
104        self.declare('load_solution', ConfigValue(domain=bool))
105        self.declare('symbolic_solver_labels', ConfigValue(domain=bool))
106        self.declare('report_timing', ConfigValue(domain=bool))
107
108        self.time_limit: Optional[float] = None
109        self.stream_solver: bool = False
110        self.load_solution: bool = True
111        self.symbolic_solver_labels: bool = False
112        self.report_timing: bool = False
113
114
115class MIPSolverConfig(SolverConfig):
116    """
117    Attributes
118    ----------
119    mip_gap: float
120        Solver will terminate if the mip gap is less than mip_gap
121    relax_integrality: bool
122        If True, all integer variables will be relaxed to continuous
123        variables before solving
124    """
125    def __init__(self,
126                 description=None,
127                 doc=None,
128                 implicit=False,
129                 implicit_domain=None,
130                 visibility=0):
131        super(MIPSolverConfig, self).__init__(description=description,
132                                              doc=doc,
133                                              implicit=implicit,
134                                              implicit_domain=implicit_domain,
135                                              visibility=visibility)
136
137        self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat))
138        self.declare('relax_integrality', ConfigValue(domain=bool))
139
140        self.mip_gap: Optional[float] = None
141        self.relax_integrality: bool = False
142
143
144class SolutionLoaderBase(abc.ABC):
145    def load_vars(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> NoReturn:
146        """
147        Load the solution of the primal variables into the value attribute of the variables.
148
149        Parameters
150        ----------
151        vars_to_load: list
152            A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution
153            to all primal variables will be loaded.
154        """
155        for v, val in self.get_primals(vars_to_load=vars_to_load).items():
156            v.value = val
157
158    @abc.abstractmethod
159    def get_primals(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
160        """
161        Returns a ComponentMap mapping variable to var value.
162
163        Parameters
164        ----------
165        vars_to_load: list
166            A list of the variables whose solution value should be retreived. If vars_to_load is None,
167            then the values for all variables will be retreived.
168
169        Returns
170        -------
171        primals: ComponentMap
172            Maps variables to solution values
173        """
174        pass
175
176    def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
177        """
178        Returns a dictionary mapping constraint to dual value.
179
180        Parameters
181        ----------
182        cons_to_load: list
183            A list of the constraints whose duals should be retreived. If cons_to_load is None, then the duals for all
184            constraints will be retreived.
185
186        Returns
187        -------
188        duals: dict
189            Maps constraints to dual values
190        """
191        raise NotImplementedError(f'{type(self)} does not support the get_duals method')
192
193    def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
194        """
195        Returns a dictionary mapping constraint to slack.
196
197        Parameters
198        ----------
199        cons_to_load: list
200            A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all
201            constraints will be loaded.
202
203        Returns
204        -------
205        slacks: dict
206            Maps constraints to slacks
207        """
208        raise NotImplementedError(f'{type(self)} does not support the get_slacks method')
209
210    def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
211        """
212        Returns a ComponentMap mapping variable to reduced cost.
213
214        Parameters
215        ----------
216        vars_to_load: list
217            A list of the variables whose reduced cost should be retreived. If vars_to_load is None, then the
218            reduced costs for all variables will be loaded.
219
220        Returns
221        -------
222        reduced_costs: ComponentMap
223            Maps variables to reduced costs
224        """
225        raise NotImplementedError(f'{type(self)} does not support the get_reduced_costs method')
226
227
228class SolutionLoader(SolutionLoaderBase):
229    def __init__(self, primals, duals, slacks, reduced_costs):
230        """
231        Parameters
232        ----------
233        primals: dict
234            maps id(Var) to (var, value)
235        duals: dict
236            maps Constraint to dual value
237        slacks: dict
238            maps Constraint to slack value
239        reduced_costs: dict
240            maps id(Var) to (var, reduced_cost)
241        """
242        self._primals = primals
243        self._duals = duals
244        self._slacks = slacks
245        self._reduced_costs = reduced_costs
246
247    def get_primals(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
248        if vars_to_load is None:
249            return ComponentMap(self._primals.values())
250        else:
251            primals = ComponentMap()
252            for v in vars_to_load:
253                primals[v] = self._primals[id(v)][1]
254
255    def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
256        if cons_to_load is None:
257            duals = dict(self._duals)
258        else:
259            duals = dict()
260            for c in cons_to_load:
261                duals[c] = self._duals[c]
262        return duals
263
264    def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
265        if cons_to_load is None:
266            slacks = dict(self._slacks)
267        else:
268            slacks = dict()
269            for c in cons_to_load:
270                slacks[c] = self._slacks[c]
271        return slacks
272
273    def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
274        if vars_to_load is None:
275            rc = ComponentMap(self._reduced_costs.values())
276        else:
277            rc = ComponentMap()
278            for v in vars_to_load:
279                rc[v] = self._reduced_costs[id(v)][1]
280        return rc
281
282
283class Results(object):
284    """
285    Attributes
286    ----------
287    termination_condition: TerminationCondition
288        The reason the solver exited. This is a member of the
289        TerminationCondition enum.
290    best_feasible_objective: float
291        If a feasible solution was found, this is the objective value of
292        the best solution found. If no feasible solution was found, this is
293        None.
294    best_objective_bound: float
295        The best objective bound found. For minimization problems, this is
296        the lower bound. For maximization problems, this is the upper bound.
297        For solvers that do not provide an objective bound, this should be -inf
298        (minimization) or inf (maximization)
299
300    Here is an example workflow:
301
302        >>> import pyomo.environ as pe
303        >>> from pyomo.contrib import appsi
304        >>> m = pe.ConcreteModel()
305        >>> m.x = pe.Var()
306        >>> m.obj = pe.Objective(expr=m.x**2)
307        >>> opt = appsi.solvers.Ipopt()
308        >>> opt.config.load_solution = False
309        >>> results = opt.solve(m) #doctest:+SKIP
310        >>> if results.termination_condition == appsi.base.TerminationCondition.optimal: #doctest:+SKIP
311        ...     print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP
312        ...     results.solution_loader.load_vars() #doctest:+SKIP
313        ...     print('the optimal value of x is ', m.x.value) #doctest:+SKIP
314        ... elif results.best_feasible_objective is not None: #doctest:+SKIP
315        ...     print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP
316        ...     results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP
317        ...     print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP
318        ... elif results.termination_condition in {appsi.base.TerminationCondition.maxIterations, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP
319        ...     print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP
320        ... else: #doctest:+SKIP
321        ...     print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP
322    """
323    def __init__(self):
324        self.solution_loader: Optional[SolutionLoaderBase] = None
325        self.termination_condition: TerminationCondition = TerminationCondition.unknown
326        self.best_feasible_objective: Optional[float] = None
327        self.best_objective_bound: Optional[float] = None
328
329    def __str__(self):
330        s = ''
331        s += 'termination_condition: '   + str(self.termination_condition)   + '\n'
332        s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n'
333        s += 'best_objective_bound: '    + str(self.best_objective_bound)
334        return s
335
336
337class UpdateConfig(ConfigDict):
338    """
339    Attributes
340    ----------
341    check_for_new_or_removed_constraints: bool
342    check_for_new_or_removed_vars: bool
343    check_for_new_or_removed_params: bool
344    update_constraints: bool
345    update_vars: bool
346    update_params: bool
347    update_named_expressions: bool
348    """
349    def __init__(self):
350        super(UpdateConfig, self).__init__()
351
352        self.declare('check_for_new_or_removed_constraints', ConfigValue(domain=bool))
353        self.declare('check_for_new_or_removed_vars', ConfigValue(domain=bool))
354        self.declare('check_for_new_or_removed_params', ConfigValue(domain=bool))
355        self.declare('update_constraints', ConfigValue(domain=bool))
356        self.declare('update_vars', ConfigValue(domain=bool))
357        self.declare('update_params', ConfigValue(domain=bool))
358        self.declare('update_named_expressions', ConfigValue(domain=bool))
359
360        self.check_for_new_or_removed_constraints: bool = True
361        self.check_for_new_or_removed_vars: bool = True
362        self.check_for_new_or_removed_params: bool = True
363        self.update_constraints: bool = True
364        self.update_vars: bool = True
365        self.update_params: bool = True
366        self.update_named_expressions: bool = True
367
368
369class Solver(abc.ABC):
370    class Availability(enum.IntEnum):
371        NotFound = 0
372        BadVersion = -1
373        BadLicense = -2
374        FullLicense = 1
375        LimitedLicense = 2
376
377        def __bool__(self):
378            return self._value_ > 0
379
380    @abc.abstractmethod
381    def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results:
382        """
383        Solve a Pyomo model.
384
385        Parameters
386        ----------
387        model: _BlockData
388            The Pyomo model to be solved
389        timer: HierarchicalTimer
390            An option timer for reporting timing
391
392        Returns
393        -------
394        results: Results
395            A results object
396        """
397        pass
398
399    @abc.abstractmethod
400    def available(self):
401        """Test if the solver is available on this system.
402
403        Nominally, this will return True if the solver interface is
404        valid and can be used to solve problems and False if it cannot.
405
406        Note that for licensed solvers there are a number of "levels" of
407        available: depending on the license, the solver may be available
408        with limitations on problem size or runtime (e.g., 'demo'
409        vs. 'community' vs. 'full').  In these cases, the solver may
410        return a subclass of enum.IntEnum, with members that resolve to
411        True if the solver is available (possibly with limitations).
412        The Enum may also have multiple members that all resolve to
413        False indicating the reason why the interface is not available
414        (not found, bad license, unsupported version, etc).
415
416        Returns
417        -------
418        available: Solver.Availability
419            An enum that indicates "how available" the solver is.
420            Note that the enum can be cast to bool, which will
421            be True if the solver is runable at all and False
422            otherwise.
423        """
424        pass
425
426    @abc.abstractmethod
427    def version(self) -> Tuple:
428        """
429        Returns
430        -------
431        version: tuple
432            A tuple representing the version
433        """
434
435    @property
436    @abc.abstractmethod
437    def config(self):
438        """
439        An object for configuring solve options.
440
441        Returns
442        -------
443        SolverConfig
444            An object for configuring pyomo solve options such as the time limit.
445            These options are mostly independent of the solver.
446        """
447        pass
448
449    @property
450    @abc.abstractmethod
451    def symbol_map(self):
452        pass
453
454    def is_persistent(self):
455        """
456        Returns
457        -------
458        is_persistent: bool
459            True if the solver is a persistent solver.
460        """
461        return False
462
463
464class PersistentSolver(Solver):
465    def is_persistent(self):
466        return True
467
468    def load_vars(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> NoReturn:
469        """
470        Load the solution of the primal variables into the value attribut of the variables.
471
472        Parameters
473        ----------
474        vars_to_load: list
475            A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution
476            to all primal variables will be loaded.
477        """
478        for v, val in self.get_primals(vars_to_load=vars_to_load).items():
479            v.value = val
480
481    @abc.abstractmethod
482    def get_primals(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
483        pass
484
485    def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
486        """
487        Declare sign convention in docstring here.
488
489        Parameters
490        ----------
491        cons_to_load: list
492            A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all
493            constraints will be loaded.
494
495        Returns
496        -------
497        duals: dict
498            Maps constraints to dual values
499        """
500        raise NotImplementedError('{0} does not support the get_duals method'.format(type(self)))
501
502    def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
503        """
504        Parameters
505        ----------
506        cons_to_load: list
507            A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all
508            constraints will be loaded.
509
510        Returns
511        -------
512        slacks: dict
513            Maps constraints to slack values
514        """
515        raise NotImplementedError('{0} does not support the get_slacks method'.format(type(self)))
516
517    def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
518        """
519        Parameters
520        ----------
521        vars_to_load: list
522            A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs
523            will be loaded.
524
525        Returns
526        -------
527        reduced_costs: ComponentMap
528            Maps variable to reduced cost
529        """
530        raise NotImplementedError('{0} does not support the get_reduced_costs method'.format(type(self)))
531
532    @property
533    @abc.abstractmethod
534    def update_config(self) -> UpdateConfig:
535        pass
536
537    @abc.abstractmethod
538    def set_instance(self, model):
539        pass
540
541    @abc.abstractmethod
542    def add_variables(self, variables: List[_GeneralVarData]):
543        pass
544
545    @abc.abstractmethod
546    def add_params(self, params: List[_ParamData]):
547        pass
548
549    @abc.abstractmethod
550    def add_constraints(self, cons: List[_GeneralConstraintData]):
551        pass
552
553    @abc.abstractmethod
554    def add_block(self, block: _BlockData):
555        pass
556
557    @abc.abstractmethod
558    def remove_variables(self, variables: List[_GeneralVarData]):
559        pass
560
561    @abc.abstractmethod
562    def remove_params(self, params: List[_ParamData]):
563        pass
564
565    @abc.abstractmethod
566    def remove_constraints(self, cons: List[_GeneralConstraintData]):
567        pass
568
569    @abc.abstractmethod
570    def remove_block(self, block: _BlockData):
571        pass
572
573    @abc.abstractmethod
574    def set_objective(self, obj: _GeneralObjectiveData):
575        pass
576
577    @abc.abstractmethod
578    def update_variables(self, variables: List[_GeneralVarData]):
579        pass
580
581    @abc.abstractmethod
582    def update_params(self):
583        pass
584
585
586class PersistentSolutionLoader(SolutionLoaderBase):
587    def __init__(self, solver: PersistentSolver):
588        self._solver = solver
589        self._valid = True
590
591    def _assert_solution_still_valid(self):
592        if not self._valid:
593            raise RuntimeError('The results in the solver are no longer valid.')
594
595    def get_primals(self, vars_to_load=None):
596        self._assert_solution_still_valid()
597        return self._solver.get_primals(vars_to_load=vars_to_load)
598
599    def get_duals(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
600        self._assert_solution_still_valid()
601        return self._solver.get_duals(cons_to_load=cons_to_load)
602
603    def get_slacks(self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None) -> Dict[_GeneralConstraintData, float]:
604        self._assert_solution_still_valid()
605        return self._solver.get_slacks(cons_to_load=cons_to_load)
606
607    def get_reduced_costs(self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None) -> Mapping[_GeneralVarData, float]:
608        self._assert_solution_still_valid()
609        return self._solver.get_reduced_costs(vars_to_load=vars_to_load)
610
611    def invalidate(self):
612        self._valid = False
613
614
615"""
616What can change in a pyomo model?
617- variables added or removed
618- constraints added or removed
619- objective changed
620- objective expr changed
621- params added or removed
622- variable modified
623  - lb
624  - ub
625  - fixed or unfixed
626  - domain
627  - value
628- constraint modified
629  - lower
630  - upper
631  - body
632  - active or not
633- named expressions modified
634  - expr
635- param modified
636  - value
637
638Ideas:
639- Consider explicitly handling deactivated constraints; favor deactivation over removal
640  and activation over addition
641
642Notes:
643- variable bounds cannot be updated with mutable params; you must call update_variables
644"""
645
646
647class PersistentBase(abc.ABC):
648    def __init__(self):
649        self._model = None
650        self._active_constraints = dict()  # maps constraint to (lower, body, upper)
651        self._vars = dict()  # maps var id to (var, lb, ub, fixed, domain, value)
652        self._params = dict()  # maps param id to param
653        self._objective = None
654        self._objective_expr = None
655        self._objective_sense = None
656        self._named_expressions = dict()  # maps constraint to list of tuples (named_expr, named_expr.expr)
657        self._obj_named_expressions = list()
658        self._update_config = UpdateConfig()
659        self._referenced_variables = dict()  # number of constraints/objectives each variable is used in
660        self._vars_referenced_by_con = dict()
661        self._vars_referenced_by_obj = list()
662
663    @property
664    def update_config(self):
665        return self._update_config
666
667    @update_config.setter
668    def update_config(self, val: UpdateConfig):
669        self._update_config = val
670
671    def set_instance(self, model):
672        saved_update_config = self.update_config
673        self.__init__()
674        self.update_config = saved_update_config
675        self._model = model
676        self.add_block(model)
677        if self._objective is None:
678            self.set_objective(None)
679
680    @abc.abstractmethod
681    def _add_variables(self, variables: List[_GeneralVarData]):
682        pass
683
684    def add_variables(self, variables: List[_GeneralVarData]):
685        for v in variables:
686            if id(v) in self._referenced_variables:
687                raise ValueError('variable {name} has already been added'.format(name=v.name))
688            self._referenced_variables[id(v)] = 0
689            self._vars[id(v)] = (v, v.lb, v.ub, v.is_fixed(), v.domain, v.value)
690        self._add_variables(variables)
691
692    @abc.abstractmethod
693    def _add_params(self, params: List[_ParamData]):
694        pass
695
696    def add_params(self, params: List[_ParamData]):
697        for p in params:
698            self._params[id(p)] = p
699        self._add_params(params)
700
701    @abc.abstractmethod
702    def _add_constraints(self, cons: List[_GeneralConstraintData]):
703        pass
704
705    def add_constraints(self, cons: List[_GeneralConstraintData]):
706        all_fixed_vars = dict()
707        for con in cons:
708            if con in self._named_expressions:
709                raise ValueError('constraint {name} has already been added'.format(name=con.name))
710            self._active_constraints[con] = (con.lower, con.body, con.upper)
711            named_exprs, variables, fixed_vars = identify_named_expressions(con.body)
712            self._named_expressions[con] = [(e, e.expr) for e in named_exprs]
713            self._vars_referenced_by_con[con] = variables
714            for v in variables:
715                self._referenced_variables[id(v)] += 1
716            for v in fixed_vars:
717                v.unfix()
718                all_fixed_vars[id(v)] = v
719        self._add_constraints(cons)
720        for v in all_fixed_vars.values():
721            v.fix()
722
723    @abc.abstractmethod
724    def _add_sos_constraints(self, cons: List[_SOSConstraintData]):
725        pass
726
727    def add_sos_constraints(self, cons: List[_SOSConstraintData]):
728        for con in cons:
729            if con in self._vars_referenced_by_con:
730                raise ValueError('constraint {name} has already been added'.format(name=con.name))
731            self._active_constraints[con] = tuple()
732            variables = con.get_variables()
733            self._named_expressions[con] = list()
734            self._vars_referenced_by_con[con] = variables
735            for v in variables:
736                self._referenced_variables[id(v)] += 1
737        self._add_sos_constraints(cons)
738
739    @abc.abstractmethod
740    def _set_objective(self, obj: _GeneralObjectiveData):
741        pass
742
743    def set_objective(self, obj: _GeneralObjectiveData):
744        if self._objective is not None:
745            for v in self._vars_referenced_by_obj:
746                self._referenced_variables[id(v)] -= 1
747        if obj is not None:
748            self._objective = obj
749            self._objective_expr = obj.expr
750            self._objective_sense = obj.sense
751            named_exprs, variables, fixed_vars = identify_named_expressions(obj.expr)
752            self._obj_named_expressions = [(i, i.expr) for i in named_exprs]
753            self._vars_referenced_by_obj = variables
754            for v in variables:
755                self._referenced_variables[id(v)] += 1
756            for v in fixed_vars:
757                v.unfix()
758            self._set_objective(obj)
759            for v in fixed_vars:
760                v.fix()
761        else:
762            self._vars_referenced_by_obj = list()
763            self._objective = None
764            self._objective_expr = None
765            self._objective_sense = None
766            self._obj_named_expressions = list()
767            self._set_objective(obj)
768
769    def add_block(self, block):
770        self.add_variables([var for var in block.component_data_objects(Var, descend_into=True, sort=False)])
771        self.add_params([p for p in block.component_data_objects(Param, descend_into=True, sort=False)])
772        self.add_constraints([con for con in block.component_data_objects(Constraint, descend_into=True,
773                                                                          active=True, sort=False)])
774        self.add_sos_constraints([con for con in block.component_data_objects(SOSConstraint, descend_into=True,
775                                                                              active=True, sort=False)])
776        obj = get_objective(block)
777        if obj is not None:
778            self.set_objective(obj)
779
780    @abc.abstractmethod
781    def _remove_constraints(self, cons: List[_GeneralConstraintData]):
782        pass
783
784    def remove_constraints(self, cons: List[_GeneralConstraintData]):
785        self._remove_constraints(cons)
786        for con in cons:
787            if con not in self._named_expressions:
788                raise ValueError('cannot remove constraint {name} - it was not added'.format(name=con.name))
789            for v in self._vars_referenced_by_con[con]:
790                self._referenced_variables[id(v)] -= 1
791            del self._active_constraints[con]
792            del self._named_expressions[con]
793            del self._vars_referenced_by_con[con]
794
795    @abc.abstractmethod
796    def _remove_sos_constraints(self, cons: List[_SOSConstraintData]):
797        pass
798
799    def remove_sos_constraints(self, cons: List[_SOSConstraintData]):
800        self._remove_sos_constraints(cons)
801        for con in cons:
802            if con not in self._vars_referenced_by_con:
803                raise ValueError('cannot remove constraint {name} - it was not added'.format(name=con.name))
804            for v in self._vars_referenced_by_con[con]:
805                self._referenced_variables[id(v)] -= 1
806            del self._active_constraints[con]
807            del self._named_expressions[con]
808            del self._vars_referenced_by_con[con]
809
810    @abc.abstractmethod
811    def _remove_variables(self, variables: List[_GeneralVarData]):
812        pass
813
814    def remove_variables(self, variables: List[_GeneralVarData]):
815        self._remove_variables(variables)
816        for v in variables:
817            if id(v) not in self._referenced_variables:
818                raise ValueError('cannot remove variable {name} - it has not been added'.format(name=v.name))
819            if self._referenced_variables[id(v)] != 0:
820                raise ValueError('cannot remove variable {name} - it is still being used by constraints or the objective'.format(name=v.name))
821            del self._referenced_variables[id(v)]
822            del self._vars[id(v)]
823
824    @abc.abstractmethod
825    def _remove_params(self, params: List[_ParamData]):
826        pass
827
828    def remove_params(self, params: List[_ParamData]):
829        self._remove_params(params)
830        for p in params:
831            del self._params[id(p)]
832
833    def remove_block(self, block):
834        self.remove_constraints([con for con in block.component_data_objects(ctype=Constraint, descend_into=True,
835                                                                             active=True, sort=False)])
836        self.remove_sos_constraints([con for con in block.component_data_objects(ctype=SOSConstraint, descend_into=True,
837                                                                                 active=True, sort=False)])
838        self.remove_variables([var for var in block.component_data_objects(ctype=Var, descend_into=True, sort=False)])
839        self.remove_params([p for p in block.component_data_objects(ctype=Param, descend_into=True, sort=False)])
840
841    @abc.abstractmethod
842    def _update_variables(self, variables: List[_GeneralVarData]):
843        pass
844
845    def update_variables(self, variables: List[_GeneralVarData]):
846        for v in variables:
847            self._vars[id(v)] = (v, v.lb, v.ub, v.is_fixed(), v.domain, v.value)
848        self._update_variables(variables)
849
850    @abc.abstractmethod
851    def update_params(self):
852        pass
853
854    def solve_sub_block(self, block):
855        raise NotImplementedError('This is just an idea right now')
856
857    def update(self, timer: HierarchicalTimer = None):
858        if timer is None:
859            timer = HierarchicalTimer()
860        config = self.update_config
861        new_vars = list()
862        old_vars = list()
863        new_params = list()
864        old_params = list()
865        new_cons = list()
866        old_cons = list()
867        old_sos = list()
868        new_sos = list()
869        current_vars_dict = dict()
870        current_cons_dict = dict()
871        current_sos_dict = dict()
872        timer.start('vars')
873        if config.check_for_new_or_removed_vars or config.update_vars:
874            current_vars_dict = {id(v): v for v in self._model.component_data_objects(Var, descend_into=True, sort=False)}
875            for v_id, v in current_vars_dict.items():
876                if v_id not in self._vars:
877                    new_vars.append(v)
878            for v_id, v_tuple in self._vars.items():
879                if v_id not in current_vars_dict:
880                    old_vars.append(v_tuple[0])
881        timer.stop('vars')
882        timer.start('params')
883        if config.check_for_new_or_removed_params:
884            current_params_dict = {id(p): p for p in self._model.component_data_objects(Param, descend_into=True, sort=False)}
885            for p_id, p in current_params_dict.items():
886                if p_id not in self._params:
887                    new_params.append(p)
888            for p_id, p in self._params.items():
889                if p_id not in current_params_dict:
890                    old_params.append(p)
891        timer.stop('params')
892        timer.start('cons')
893        if config.check_for_new_or_removed_constraints or config.update_constraints:
894            current_cons_dict = {c: None for c in self._model.component_data_objects(Constraint, descend_into=True, active=True, sort=False)}
895            current_sos_dict = {c: None for c in self._model.component_data_objects(SOSConstraint, descend_into=True, active=True, sort=False)}
896            for c in current_cons_dict.keys():
897                if c not in self._vars_referenced_by_con:
898                    new_cons.append(c)
899            for c in current_sos_dict.keys():
900                if c not in self._vars_referenced_by_con:
901                    new_sos.append(c)
902            for c in self._vars_referenced_by_con.keys():
903                if c not in current_cons_dict and c not in current_sos_dict:
904                    if (c.ctype is Constraint) or (c.ctype is None and isinstance(c, _GeneralConstraintData)):
905                        old_cons.append(c)
906                    else:
907                        assert (c.ctype is SOSConstraint) or (c.ctype is None and isinstance(c, _SOSConstraintData))
908                        old_sos.append(c)
909        self.remove_constraints(old_cons)
910        self.remove_sos_constraints(old_sos)
911        timer.stop('cons')
912        timer.start('vars')
913        self.remove_variables(old_vars)
914        timer.stop('vars')
915        timer.start('params')
916        self.remove_params(old_params)
917
918        # sticking this between removal and addition
919        # is important so that we don't do unnecessary work
920        if config.update_params:
921            self.update_params()
922
923        self.add_params(new_params)
924        timer.stop('params')
925        timer.start('vars')
926        self.add_variables(new_vars)
927        timer.stop('vars')
928        timer.start('cons')
929        self.add_constraints(new_cons)
930        self.add_sos_constraints(new_sos)
931        new_cons_set = set(new_cons)
932        new_sos_set = set(new_sos)
933        new_vars_set = set(id(v) for v in new_vars)
934        if config.update_constraints:
935            cons_to_update = list()
936            sos_to_update = list()
937            for c in current_cons_dict.keys():
938                if c not in new_cons_set:
939                    cons_to_update.append(c)
940            for c in current_sos_dict.keys():
941                if c not in new_sos_set:
942                    sos_to_update.append(c)
943            cons_to_remove_and_add = list()
944            for c in cons_to_update:
945                lower, body, upper = self._active_constraints[c]
946                if c.lower is not lower or c.body is not body or c.upper is not upper:
947                    cons_to_remove_and_add.append(c)
948            self.remove_constraints(cons_to_remove_and_add)
949            self.add_constraints(cons_to_remove_and_add)
950            self.remove_sos_constraints(sos_to_update)
951            self.add_sos_constraints(sos_to_update)
952        timer.stop('cons')
953        timer.start('vars')
954        if config.update_vars:
955            vars_to_check = list()
956            for v_id, v in current_vars_dict.items():
957                if v_id not in new_vars_set:
958                    vars_to_check.append(v)
959            vars_to_update = list()
960            for v in vars_to_check:
961                _v, lb, ub, fixed, domain, value = self._vars[id(v)]
962                if lb is not v.lb:
963                    vars_to_update.append(v)
964                elif ub is not v.ub:
965                    vars_to_update.append(v)
966                elif fixed is not v.is_fixed():
967                    vars_to_update.append(v)
968                elif domain is not v.domain:
969                    vars_to_update.append(v)
970                elif fixed and (value is not v.value):
971                    vars_to_update.append(v)
972            self.update_variables(vars_to_update)
973        timer.stop('vars')
974        timer.start('named expressions')
975        if config.update_named_expressions:
976            cons_to_update = list()
977            for c, expr_list in self._named_expressions.items():
978                if c in new_cons_set:
979                    continue
980                for named_expr, old_expr in expr_list:
981                    if named_expr.expr is not old_expr:
982                        cons_to_update.append(c)
983                        break
984            self.remove_constraints(cons_to_update)
985            self.add_constraints(cons_to_update)
986        timer.stop('named expressions')
987        timer.start('objective')
988        pyomo_obj = get_objective(self._model)
989        need_to_set_objective = False
990        if pyomo_obj is not self._objective:
991            need_to_set_objective = True
992        elif pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr:
993            need_to_set_objective = True
994        elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense:
995            need_to_set_objective = True
996        elif config.update_named_expressions:
997            for named_expr, old_expr in self._obj_named_expressions:
998                if named_expr.expr is not old_expr:
999                    need_to_set_objective = True
1000                    break
1001        if need_to_set_objective:
1002            self.set_objective(pyomo_obj)
1003        timer.stop('objective')
1004
1005
1006legacy_termination_condition_map = {
1007    TerminationCondition.unknown: LegacyTerminationCondition.unknown,
1008    TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit,
1009    TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations,
1010    TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue,
1011    TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength,
1012    TerminationCondition.optimal: LegacyTerminationCondition.optimal,
1013    TerminationCondition.unbounded: LegacyTerminationCondition.unbounded,
1014    TerminationCondition.infeasible: LegacyTerminationCondition.infeasible,
1015    TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded,
1016    TerminationCondition.error: LegacyTerminationCondition.error,
1017    TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt,
1018    TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems
1019}
1020
1021
1022legacy_solver_status_map = {
1023    TerminationCondition.unknown: LegacySolverStatus.unknown,
1024    TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted,
1025    TerminationCondition.maxIterations: LegacySolverStatus.aborted,
1026    TerminationCondition.objectiveLimit: LegacySolverStatus.aborted,
1027    TerminationCondition.minStepLength: LegacySolverStatus.error,
1028    TerminationCondition.optimal: LegacySolverStatus.ok,
1029    TerminationCondition.unbounded: LegacySolverStatus.error,
1030    TerminationCondition.infeasible: LegacySolverStatus.error,
1031    TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error,
1032    TerminationCondition.error: LegacySolverStatus.error,
1033    TerminationCondition.interrupted: LegacySolverStatus.aborted,
1034    TerminationCondition.licensingProblems: LegacySolverStatus.error
1035}
1036
1037
1038legacy_solution_status_map = {
1039    TerminationCondition.unknown: LegacySolutionStatus.unknown,
1040    TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit,
1041    TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit,
1042    TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit,
1043    TerminationCondition.minStepLength: LegacySolutionStatus.error,
1044    TerminationCondition.optimal: LegacySolutionStatus.optimal,
1045    TerminationCondition.unbounded: LegacySolutionStatus.unbounded,
1046    TerminationCondition.infeasible: LegacySolutionStatus.infeasible,
1047    TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure,
1048    TerminationCondition.error: LegacySolutionStatus.error,
1049    TerminationCondition.interrupted: LegacySolutionStatus.error,
1050    TerminationCondition.licensingProblems: LegacySolutionStatus.error
1051}
1052
1053
1054class LegacySolverInterface(object):
1055    def solve(self,
1056              model: _BlockData,
1057              tee: bool = False,
1058              load_solutions: bool = True,
1059              logfile: Optional[str] = None,
1060              solnfile: Optional[str] = None,
1061              timelimit: Optional[float] = None,
1062              report_timing: bool = False,
1063              solver_io: Optional[str] = None,
1064              suffixes: Optional[Sequence] = None,
1065              options: Optional[Dict] = None,
1066              keepfiles: bool = False,
1067              symbolic_solver_labels: bool = False):
1068        original_config = self.config
1069        self.config = self.config()
1070        self.config.stream_solver = tee
1071        self.config.load_solution = load_solutions
1072        self.config.symbolic_solver_labels = symbolic_solver_labels
1073        self.config.time_limit = timelimit
1074        self.config.report_timing = report_timing
1075        if solver_io is not None:
1076            raise NotImplementedError('Still working on this')
1077        if suffixes is not None:
1078            raise NotImplementedError('Still working on this')
1079        if logfile is not None:
1080            raise NotImplementedError('Still working on this')
1081        if 'keepfiles' in self.config:
1082            self.config.keepfiles = keepfiles
1083        if solnfile is not None:
1084            if 'filename' in self.config:
1085                filename = os.path.splitext(solnfile)[0]
1086                self.config.filename = filename
1087        original_options = self.options
1088        if options is not None:
1089            self.options = options
1090
1091        results: Results = super(LegacySolverInterface, self).solve(model)
1092
1093        legacy_results = LegacySolverResults()
1094        legacy_soln = LegacySolution()
1095        legacy_results.solver.status = legacy_solver_status_map[results.termination_condition]
1096        legacy_results.solver.termination_condition = legacy_termination_condition_map[results.termination_condition]
1097        legacy_soln.status = legacy_solution_status_map[results.termination_condition]
1098        legacy_results.solver.termination_message = str(results.termination_condition)
1099
1100        obj = get_objective(model)
1101        legacy_results.problem.sense = obj.sense
1102
1103        if obj.sense == minimize:
1104            legacy_results.problem.lower_bound = results.best_objective_bound
1105            legacy_results.problem.upper_bound = results.best_feasible_objective
1106        else:
1107            legacy_results.problem.upper_bound = results.best_objective_bound
1108            legacy_results.problem.lower_bound = results.best_feasible_objective
1109        if results.best_feasible_objective is not None and results.best_objective_bound is not None:
1110            legacy_soln.gap = abs(results.best_feasible_objective - results.best_objective_bound)
1111        else:
1112            legacy_soln.gap = None
1113
1114        symbol_map = SymbolMap()
1115        symbol_map.byObject = dict(self.symbol_map.byObject)
1116        symbol_map.bySymbol = {symb: weakref.ref(obj()) for symb, obj in self.symbol_map.bySymbol.items()}
1117        symbol_map.aliases = {symb: weakref.ref(obj()) for symb, obj in self.symbol_map.aliases.items()}
1118        symbol_map.default_labeler = self.symbol_map.default_labeler
1119        model.solutions.add_symbol_map(symbol_map)
1120        legacy_results._smap_id = id(symbol_map)
1121
1122        delete_legacy_soln = True
1123        if load_solutions:
1124            if hasattr(model, 'dual') and model.dual.import_enabled():
1125                for c, val in results.solution_loader.get_duals().items():
1126                    model.dual[c] = val
1127            if hasattr(model, 'slack') and model.slack.import_enabled():
1128                for c, val in results.solution_loader.get_slacks().items():
1129                    model.slack[c] = val
1130            if hasattr(model, 'rc') and model.rc.import_enabled():
1131                for v, val in results.solution_loader.get_reduced_costs().items():
1132                    model.rc[v] = val
1133        elif results.best_feasible_objective is not None:
1134            delete_legacy_soln = False
1135            for v, val in results.solution_loader.get_primals().items():
1136                legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val}
1137            if hasattr(model, 'dual') and model.dual.import_enabled():
1138                for c, val in results.solution_loader.get_duals().items():
1139                    legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val}
1140            if hasattr(model, 'slack') and model.slack.import_enabled():
1141                for c, val in results.solution_loader.get_slacks().items():
1142                    symbol = symbol_map.getSymbol(c)
1143                    if symbol in legacy_soln.constraint:
1144                        legacy_soln.constraint[symbol]['Slack'] = val
1145            if hasattr(model, 'rc') and model.rc.import_enabled():
1146                for v, val in results.solution_loader.get_reduced_costs().items():
1147                    legacy_soln.variable['Rc'] = val
1148
1149        legacy_results.solution.insert(legacy_soln)
1150        if delete_legacy_soln:
1151            legacy_results.solution.delete(0)
1152
1153        self.config = original_config
1154        self.options = original_options
1155
1156        return legacy_results
1157
1158    def available(self, exception_flag=True):
1159        ans = super(LegacySolverInterface, self).available()
1160        if exception_flag and not ans:
1161            raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).')
1162        return bool(ans)
1163
1164    def license_is_valid(self) -> bool:
1165        """Test if the solver license is valid on this system.
1166
1167        Note that this method is included for compatibility with the
1168        legacy SolverFactory interface.  Unlicensed or open source
1169        solvers will return True by definition.  Licensed solvers will
1170        return True if a valid license is found.
1171
1172        Returns
1173        -------
1174        available: bool
1175            True if the solver license is valid. Otherwise, False.
1176
1177        """
1178        return bool(self.available())
1179
1180    @property
1181    def options(self):
1182        for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc']:
1183            if hasattr(self, solver_name + '_options'):
1184                return getattr(self, solver_name + '_options')
1185        raise NotImplementedError('Could not find the correct options')
1186
1187    @options.setter
1188    def options(self, val):
1189        found = False
1190        for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc']:
1191            if hasattr(self, solver_name + '_options'):
1192                setattr(self, solver_name + '_options', val)
1193                found = True
1194        if not found:
1195            raise NotImplementedError('Could not find the correct options')
1196
1197
1198    def __enter__(self):
1199        return self
1200
1201    def __exit__(self, t, v, traceback):
1202        pass
1203
1204
1205class SolverFactoryClass(Factory):
1206    def register(self, name, doc=None):
1207        def decorator(cls):
1208            self._cls[name] = cls
1209            self._doc[name] = doc
1210
1211            class LegacySolver(LegacySolverInterface, cls):
1212                pass
1213            LegacySolverFactory.register(name, doc)(LegacySolver)
1214
1215            return cls
1216        return decorator
1217
1218
1219SolverFactory = SolverFactoryClass()
1220