Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding logical_to_disjunctive, moving GDP transformations from logical-to-linear to logical-to-disjunctive #2627

Merged
merged 57 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
1e911c5
Beginnings of new to-CNF walker for logical to linear
emma58 Nov 11, 2022
d2c3e4c
Implementing the remaining handlers
emma58 Nov 17, 2022
ee91da8
Adding tests for the logical to linear visitor
emma58 Nov 17, 2022
45ccf5c
Realizing that I don't yet know how to handle non-constant first argu…
emma58 Nov 17, 2022
bd8aa88
Rewrite of logical to linear transformation with new walker. No idea …
emma58 Nov 17, 2022
3ace928
Merge branch 'main' into logical-to-linear-rewrite
emma58 Nov 17, 2022
102319c
Reverting core.logical_to_linear transformation
emma58 Nov 18, 2022
52700a8
Deprecating logical to linear
emma58 Nov 18, 2022
f01f8e5
Renaming the new logical_to_linear to be logical_to_disjunctive
emma58 Nov 18, 2022
b90024d
Nothing to see here...
emma58 Nov 18, 2022
711cff6
Punting on non-integer args to atmost atleast and exactly
emma58 Nov 18, 2022
36d68d4
Adding more tests
emma58 Nov 18, 2022
27f808a
Cleaning up and testing the logical-to-disjunctive transformation
emma58 Nov 19, 2022
4a548c9
Need to figure out which comes first: logical to linear or preprocess…
emma58 Nov 20, 2022
63afe4e
Logical to disjunctive descends into Disjuncts
emma58 Nov 21, 2022
9af5a82
BigM transformation calls logical to disjunctive, starting to fix tests
emma58 Nov 21, 2022
b40922f
Having native types compare as equal without having the same class in…
emma58 Nov 21, 2022
ae09b77
Removing debugging
emma58 Nov 23, 2022
efb997e
Correctly calculating targets for logical to disjunctive call from bigm
emma58 Nov 23, 2022
1d6f706
Rewriting bigm test testing that logical constraints are transformed
emma58 Nov 24, 2022
81861b7
Switching hull to use logical_to_disjunctive and rewriting the corres…
emma58 Nov 25, 2022
5085bf1
Switching multiple bigm to logical_to_disjunctive, rewriting the test
emma58 Nov 25, 2022
6e33f83
Updating comment
emma58 Nov 25, 2022
9763f09
Removing special handling for _DeprecatedImplicitBinaryVariables in G…
emma58 Nov 25, 2022
f8ef97f
Removing unncessary check for sympy
emma58 Nov 25, 2022
74caa9a
Merging main
emma58 Nov 25, 2022
1c5c970
Having fix_disjuncts call bigm by defualt after it calls logical_to_d…
emma58 Nov 25, 2022
3c17da5
Merge branch 'main' into logical-to-linear-rewrite
jsiirola Nov 28, 2022
ca0a2b1
resolving merge conflicts
emma58 Nov 28, 2022
b5de277
Merge branch 'logical-to-linear-rewrite' of github.com:emma58/pyomo i…
emma58 Nov 28, 2022
0e2b82c
Switching branch and bound to call logical_to_disjunctive
emma58 Nov 28, 2022
5fa4944
Don't need to skip tests based on sympy
emma58 Nov 28, 2022
afe4ec0
Switching the OA algorithms to logical_to_disjunctive
emma58 Nov 28, 2022
0b962c4
Better debugging output in GDPopt
emma58 Nov 28, 2022
2372f6a
We cannot fix variables in the logical-to-disjunctive walker because …
emma58 Nov 28, 2022
b9b886c
Fixing tests after the change for constraining the expression to be t…
emma58 Nov 28, 2022
020c3f4
Fixing bigm tests to not assume binaries are fixed
emma58 Nov 28, 2022
3154dc5
fixing multiple bigm tests
emma58 Nov 28, 2022
420d14a
Testing the constraints requiring logical stuff to be true in hull
emma58 Nov 28, 2022
a88b395
Allowing duck typing in before child handlers
emma58 Nov 28, 2022
8251de2
Merge branch 'main' into logical-to-linear-rewrite
emma58 Nov 28, 2022
1669eb1
Not registering logical_to_linear with the rest of the core transform…
emma58 Nov 28, 2022
dcfb7ff
Registering the logical to disjunctive transformation when you import…
emma58 Nov 28, 2022
857cdc5
Adding a greatly missed init file
emma58 Nov 28, 2022
5b53c50
Defer populating element_getattr_dispatcher until cp has been imported
jsiirola Nov 29, 2022
96c4dc5
Merge branch 'main' into logical-to-linear-rewrite
jsiirola Nov 29, 2022
013a007
Resolve merge error
jsiirola Nov 29, 2022
706865d
Fix recursion when resolving docplex.cp module
jsiirola Nov 29, 2022
245b3ca
Add context manager interface (required by pyomo script)
jsiirola Nov 29, 2022
e635433
Restoring registration of core.logical_to_linear; reverting 245b3ca
jsiirola Nov 29, 2022
79fb642
Update deprecation of core.loogical_to_linear
jsiirola Nov 29, 2022
dcd3179
Defer populating dispatchers until docplex is loaded
jsiirola Nov 30, 2022
88b9d41
Update test to explicitly call logical_to_linear
jsiirola Nov 30, 2022
600996a
Updating documentation
jsiirola Nov 30, 2022
971ddc5
Use \veebar for \underline{\lor}
jsiirola Nov 30, 2022
a9c6b2b
Registering contrib.cp plugins with pyomo.environ
jsiirola Nov 30, 2022
5e885ca
Un-deprecating logical to linear (as a whole)
emma58 Dec 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/OnlineDocs/modeling_extensions/gdp/concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ These logical propositions can include:
.. |equiv| replace:: :math:`Y_1 \Leftrightarrow Y_2`
.. |land| replace:: :math:`Y_1 \land Y_2`
.. |lor| replace:: :math:`Y_1 \lor Y_2`
.. |xor| replace:: :math:`Y_1 \underline{\lor} Y_2`
.. |xor| replace:: :math:`Y_1 \veebar Y_2`
.. |impl| replace:: :math:`Y_1 \Rightarrow Y_2`

+-----------------+---------+-------------+-------------+-------------+
Expand Down
2 changes: 1 addition & 1 deletion doc/OnlineDocs/modeling_extensions/gdp/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ These can be expressed as a disjunction as follows:
\text{constraints} \\
\text{for }\textit{else}
\end{gathered}\right] \\
Y_1 \underline{\vee} Y_2
Y_1 \veebar Y_2
\end{gather*}

Here, if the Boolean :math:`Y_1` is ``True``, then the constraints in the first disjunct are enforced; otherwise, the constraints in the second disjunct are enforced.
Expand Down
30 changes: 16 additions & 14 deletions doc/OnlineDocs/modeling_extensions/gdp/modeling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ When the ``Disjunction`` object constructor is passed a list of lists, the outer

By default, Pyomo.GDP ``Disjunction`` objects enforce an implicit "exactly one" relationship among the selection of the disjuncts (generalization of exclusive-OR).
That is, exactly one of the ``Disjunct`` indicator variables should take a ``True`` value.
This can be seen as an implicit logical proposition, in our example, :math:`Y_1 \underline{\lor} Y_2`.
This can be seen as an implicit logical proposition, in our example, :math:`Y_1 \veebar Y_2`.

Logical Propositions
====================
Expand Down Expand Up @@ -139,6 +139,11 @@ Pyomo.GDP logical expression system supported operators and their usage are list
| Equivalence | | :code:`Y[1].equivalent_to(Y[2])` | :code:`equivalent(Y[1], Y[2])` |
+--------------+------------------------+-----------------------------------+--------------------------------+

.. note::

We omit support for most infix operators, e.g. :code:`Y[1] >> Y[2]`, due to concerns about non-intuitive Python operator precedence.
That is :code:`Y[1] | Y[2] >> Y[3]` would translate to :math:`Y_1 \lor (Y_2 \Rightarrow Y_3)` rather than :math:`(Y_1 \lor Y_2) \Rightarrow Y_3`

In addition, the following constraint-programming-inspired operators are provided: ``exactly``, ``atmost``, and ``atleast``.
These predicates enforce, respectively, that exactly, at most, or at least N of their ``BooleanVar`` arguments are ``True``.

Expand All @@ -148,26 +153,23 @@ Usage:
- :code:`atmost(3, Y)`
- :code:`exactly(3, Y)`

.. note::

We omit support for most infix operators, e.g. :code:`Y[1] >> Y[2]`, due to concerns about non-intuitive Python operator precedence.
That is :code:`Y[1] | Y[2] >> Y[3]` would translate to :math:`Y_1 \lor (Y_2 \Rightarrow Y_3)` rather than :math:`(Y_1 \lor Y_2) \Rightarrow Y_3`

.. doctest::

>>> m = ConcreteModel()
>>> m.my_set = RangeSet(4)
>>> m.Y = BooleanVar(m.my_set)
>>> m.p = LogicalConstraint(expr=atleast(3, m.Y))
>>> TransformationFactory('core.logical_to_linear').apply_to(m)
>>> m.logic_to_linear.transformed_constraints.pprint() # constraint auto-generated by transformation
transformed_constraints : Size=1, Index=logic_to_linear.transformed_constraints_index, Active=True
Key : Lower : Body : Upper : Active
1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True
>>> m.p.pprint()
p : Size=1, Index=None, Active=False
Key : Body : Active
None : atleast(3: [Y[1], Y[2], Y[3], Y[4]]) : False
>>> TransformationFactory('core.logical_to_linear').apply_to(m)
...
>>> # constraint auto-generated by transformation
>>> m.logic_to_linear.transformed_constraints.pprint()
transformed_constraints : Size=1, Index=logic_to_linear.transformed_constraints_index, Active=True
Key : Lower : Body : Upper : Active
1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True

We elaborate on the ``logical_to_linear`` transformation :ref:`on the next page <gdp-reformulations>`.

Expand Down Expand Up @@ -223,8 +225,8 @@ Here, we demonstrate this capability with a toy example:
\min~&x\\
\text{s.t.}~&\left[\begin{gathered}Y_1\\x \geq 2\end{gathered}\right] \vee \left[\begin{gathered}Y_2\\x \geq 3\end{gathered}\right]\\
&\left[\begin{gathered}Y_3\\x \leq 8\end{gathered}\right] \vee \left[\begin{gathered}Y_4\\x = 2.5\end{gathered}\right] \\
&Y_1 \underline{\vee} Y_2\\
&Y_3 \underline{\vee} Y_4\\
&Y_1 \veebar Y_2\\
&Y_3 \veebar Y_4\\
&Y_1 \Rightarrow Y_4

.. doctest::
Expand Down Expand Up @@ -359,7 +361,7 @@ In the ``logical_to_linear`` transformation, we automatically convert these spec
Additional Examples
===================

The following models all work and are equivalent for :math:`\left[x = 0\right] \underline{\lor} \left[y = 0\right]`:
The following models all work and are equivalent for :math:`\left[x = 0\right] \veebar \left[y = 0\right]`:

.. doctest::

Expand Down
49 changes: 41 additions & 8 deletions doc/OnlineDocs/modeling_extensions/gdp/solving.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,25 @@ Logical constraints

.. note::

Historically it was required to convert logical propositions to
algebraic form prior to use of the MI(N)LP reformulations and the
GDPopt solver. However, this is mathematically incorrect since these
reformulations convert logical formulations to algebraic formulations.
It is therefore recommended to use both the MI(N)LP reformulations
and GDPopt directly to transform or solve GDPs that include logical
propositions.
Historically users needed to explicitly convert logical propositions
to algebraic form prior to invoking the GDP MI(N)LP reformulations
or the GDPopt solver. However, this is mathematically incorrect
since the GDP MI(N)LP reformulations themselves convert logical
formulations to algebraic formulations. The current recommended
practice is to pass the entire (mixed logical / algebraic) model to
the MI(N)LP reformulations or GDPopt directly.

There are several approaches to convert logical constraints into
algebraic form.

Conjunctive Normal Form
^^^^^^^^^^^^^^^^^^^^^^^

The following transforms logical propositions on the model to algebraic form:
The first transformation (`core.logical_to_linear`) leverages the
`sympy` package to generate the conjunctive normal form of the logical
constraints and then adds the equivalent as a list algebraic
constraints. The following transforms logical propositions on the model
to algebraic form:

.. code::

Expand All @@ -61,6 +71,29 @@ Following solution of the GDP model, values of the Boolean variables may be upda

.. autofunction:: pyomo.core.plugins.transform.logical_to_linear.update_boolean_vars_from_binary

Factorable Programming
^^^^^^^^^^^^^^^^^^^^^^

The second transformation (`contrib.logical_to_disjunctive`) leverages
ideas from factorable programming to first generate an equivalent set of
"factored" logical constraints form by traversing each logical
proposition and replacing each logical operator with an additional
Boolean variable and then adding the "simple" logical constraint that
equates the new Boolean variable with the single logical operator.

The resulting "simple" logical constraints are converted to either MIP
or GDP form: if the constraint contains only Boolean variables, then
then MIP representation is emitted. Logical constraints with mixed
integer-Boolean arguments (e.g., `atmost`, `atleast`, `exactly`, etc.)
are converted to a disjunctive representation.

As this transformation both avoids the conversion into `sympy` and only
requires a single traversal of each logical constraint,
`contrib.logical_to_disjunctive` is significantly faster than
`core.logical_to_linear` at the cost of a larger model. In practice,
the cost of the larger model is negated by the effectiveness of the MIP
presolve in most solvers.

Reformulation to MI(N)LP
------------------------

Expand Down
20 changes: 20 additions & 0 deletions pyomo/common/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,23 @@ class TempfileContextError(PyomoException, IndexError):

"""
pass


class MouseTrap(PyomoException, NotImplementedError):
"""
Exception class used to throw errors for not-implemented functionality
that might be rational to support (i.e., we already gave you a cookie)
but risks taking Pyomo's flexibility a step beyond what is sane,
or solvable, or communicable to a solver, etc. (i.e., Really? Now you
want a glass of milk too?)
"""
def __init__(self, val):
self.parameter = val

def __str__(self):
return ("Sorry, mouse, no cookies here!\n\t%s\n"
"\tThis is functionality we think may be rational to "
"support, but is not yet implemented since it might go "
"beyond what can practically be solved. However, please "
"feed the mice: pull requests are always welcome!"
% (repr(self.parameter), ))
2 changes: 2 additions & 0 deletions pyomo/contrib/cp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
DocplexWriter, CPOptimizerSolver)
from pyomo.contrib.cp.scheduling_expr.step_function_expressions import (
AlwaysIn, Step, Pulse)
# register logical_to_disjunctive transformation
import pyomo.contrib.cp.transform.logical_to_disjunctive_program
15 changes: 15 additions & 0 deletions pyomo/contrib/cp/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# ___________________________________________________________________________
#
# 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.
# ___________________________________________________________________________

def load():
from . import interval_var
from .repn import docplex_writer
from .transform import logical_to_disjunctive_program
64 changes: 36 additions & 28 deletions pyomo/contrib/cp/repn/docplex_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
# ___________________________________________________________________________

from pyomo.common.dependencies import attempt_import
cp, docplex_available = attempt_import('docplex.cp.model')
cp_solver, docplex_available = attempt_import('docplex.cp.solver')

import itertools
import logging
Expand Down Expand Up @@ -68,6 +66,33 @@
from pyomo.network import Port
###

def _finalize_docplex(module, available):
if not available:
return
_deferred_element_getattr_dispatcher['start_time'] = module.start_of
_deferred_element_getattr_dispatcher['end_time'] = module.end_of
_deferred_element_getattr_dispatcher['length'] = module.length_of
_deferred_element_getattr_dispatcher['is_present'] = module.presence_of

# Scheduling dispatchers
_before_dispatchers[_START_TIME, _START_TIME] = module.start_before_start
_before_dispatchers[_START_TIME, _END_TIME] = module.start_before_end
_before_dispatchers[_END_TIME, _START_TIME] = module.end_before_start
_before_dispatchers[_END_TIME, _END_TIME] = module.end_before_end

_at_dispatchers[_START_TIME, _START_TIME] = module.start_at_start
_at_dispatchers[_START_TIME, _END_TIME] = module.start_at_end
_at_dispatchers[_END_TIME, _START_TIME] = module.end_at_start
_at_dispatchers[_END_TIME, _END_TIME] = module.end_at_end

_time_point_dispatchers[_START_TIME] = module.start_of
_time_point_dispatchers[_END_TIME] = module.end_of


cp, docplex_available = attempt_import(
'docplex.cp.model', callback=_finalize_docplex)
cp_solver, docplex_available = attempt_import('docplex.cp.solver')

logger = logging.getLogger('pyomo.contrib.cp')

# These are things that don't need special handling:
Expand Down Expand Up @@ -217,13 +242,8 @@ def _handle_getitem(visitor, node, *data):
'xor': _XOR,
'equivalent_to': _EQUIVALENT_TO,
}
if docplex_available:
_deferred_element_getattr_dispatcher = {
'start_time': cp.start_of,
'end_time': cp.end_of,
'length': cp.length_of,
'is_present': cp.presence_of,
}
# This will get populated when cp is finally imported
_deferred_element_getattr_dispatcher = {}

def _handle_getattr(visitor, node, obj, attr):
# We either end up here because we do not yet know the list of variables to
Expand Down Expand Up @@ -676,25 +696,13 @@ def _handle_call(visitor, node, *args):
# Scheduling
##

if docplex_available:
_before_dispatchers = {
(_START_TIME, _START_TIME): cp.start_before_start,
(_START_TIME, _END_TIME): cp.start_before_end,
(_END_TIME, _START_TIME): cp.end_before_start,
(_END_TIME, _END_TIME): cp.end_before_end,
}
_at_dispatchers = {
(_START_TIME, _START_TIME): cp.start_at_start,
(_START_TIME, _END_TIME): cp.start_at_end,
(_END_TIME, _START_TIME): cp.end_at_start,
(_END_TIME, _END_TIME): cp.end_at_end
}
_time_point_dispatchers = {
_START_TIME: cp.start_of,
_END_TIME: cp.end_of,
_GENERAL: lambda x: x,
_ELEMENT_CONSTRAINT: lambda x: x,
}
# This will get populated when cp is finally imported
_before_dispatchers = {}
_at_dispatchers = {}
_time_point_dispatchers = {
_GENERAL: lambda x: x,
_ELEMENT_CONSTRAINT: lambda x: x,
}

_non_precedence_types = {_GENERAL, _ELEMENT_CONSTRAINT}

Expand Down
Loading