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 (parameterized) linear programming dual transformation! #3402

Open
wants to merge 53 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
c2b42b5
Initializing dual transform
emma58 Mar 13, 2024
8fccf37
Whoops, adding LP dual transform and tests
emma58 Mar 13, 2024
40925d0
Merge branch 'main' into lp-dual
emma58 May 21, 2024
ab58d80
First draft of 'mixed' LP dual as well as adding option for parameter…
emma58 May 22, 2024
601471f
Merge branch 'linear-walker-wrt' into lp-dual
emma58 May 22, 2024
16fdff4
Beginning of mixed form LP dal, no parameterized, with tests that don…
emma58 May 23, 2024
a1cb793
Fixing a bug with the direction of constraints being transposed, also…
emma58 Jun 12, 2024
eb44531
Making Constraint expressions prettier and testing them
emma58 Jun 12, 2024
8420e05
black
emma58 Jun 12, 2024
d702d57
Adding a test that we can recover the primal from the dual with the t…
emma58 Jun 12, 2024
ad0ef8e
Testing that dual values reported by solver and what we get from solv…
emma58 Jun 12, 2024
06c87c9
Not bothering to transpose primal constraint matrix because we don't …
emma58 Jun 21, 2024
e237311
Centralizing the var list domain validator in pyomo/util
emma58 Jun 21, 2024
d3fdccb
Modularizing standard form a little so that I can override the parts …
emma58 Jun 21, 2024
73e07f0
Draft of scipy-style CSR and CSC matrices that accomodate pyomo expre…
emma58 Jun 24, 2024
27e09f5
Rewriting LP dual code to actually use a CSC matrix sanely
emma58 Jun 24, 2024
fd276d0
whoops, missing file from two commits ago
emma58 Jun 24, 2024
ff38080
Implementing todense, making the data structures in CSC and CSR numpy…
emma58 Jun 25, 2024
8d582c9
Start on unit tests for parameterized standard form
emma58 Jun 25, 2024
4c32954
Blacking
emma58 Jun 25, 2024
8f87684
More unit tests (and whitespace)
emma58 Jun 25, 2024
ced3faa
Adding a test for todense and removing my hacky debugging in paramete…
emma58 Jun 25, 2024
0c9567a
Making _csc_to_nonnegative_vars a method on the class so that it can …
emma58 Jun 25, 2024
d0d9abf
Testing all the config variations for parameterized standard form
emma58 Jun 28, 2024
39f6e40
Black
emma58 Jun 28, 2024
7173461
Renaming _get_data_list to _to_vector
emma58 Jul 2, 2024
0a5663a
Partial fix merge of templatized-writer branch. Standard repn works b…
emma58 Jul 2, 2024
89defeb
Generalizing standard form to handle parameterized standard form again
emma58 Jul 2, 2024
18c848c
black
emma58 Jul 2, 2024
6548f29
NFC: cleaning up comments
emma58 Jul 2, 2024
40fbad3
Resolving merge conflicts with the create_csc abstraction
emma58 Jul 2, 2024
22a1f79
Merge branch 'main' into lp-dual
emma58 Jul 8, 2024
470de80
Adding a copy of scipy's sum duplicates code, but I think we can chea…
emma58 Jul 8, 2024
d1bf1b4
Merge branch 'templatized-writer' into lp-dual
emma58 Jul 8, 2024
3edc964
Removing an unused method
emma58 Jul 8, 2024
53ea29e
Merging with templatized-writer branch
emma58 Jul 9, 2024
c52a915
Fixing conflicts with paramaterized linear walker PR
emma58 Jul 10, 2024
d86e00b
Adjusting parameterized standard form to match the changes in standar…
emma58 Jul 11, 2024
c569897
black
emma58 Jul 11, 2024
70d1fbe
Converting the c matrix dense for now to get things working
emma58 Jul 11, 2024
d7d3b74
Adding a test of a parameterized dual
emma58 Jul 11, 2024
82326af
black
emma58 Jul 11, 2024
76ac53d
Merging main
emma58 Nov 4, 2024
6f5790a
Converting to var_recorder argument for parameterized standard form
emma58 Nov 4, 2024
139d307
black
emma58 Nov 4, 2024
2f40eb9
More black
emma58 Nov 4, 2024
5a25e2a
NFC: fixing comment typo
emma58 Nov 4, 2024
c2c5c0b
Adding docstrings for mapping API and clearning up some prepositional…
emma58 Nov 4, 2024
7238c31
Adding tests for paraemeterized dual from solving primal and dual, ad…
emma58 Nov 4, 2024
d914e52
Black
emma58 Nov 4, 2024
d92d4df
NFC: fixing two typos
emma58 Nov 4, 2024
662fbfb
Reverting unnecessary formatting changes in standard form
emma58 Nov 5, 2024
8f07edb
Merge branch 'main' into lp-dual
emma58 Nov 5, 2024
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
20 changes: 2 additions & 18 deletions pyomo/contrib/fme/fourier_motzkin_elimination.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from pyomo.repn.standard_repn import generate_standard_repn
from pyomo.common.collections import ComponentMap, ComponentSet
from pyomo.opt import TerminationCondition
from pyomo.util.var_list_domain import var_component_set

import logging

Expand Down Expand Up @@ -57,23 +58,6 @@ def _check_var_bounds_filter(constraint):
return True


def vars_to_eliminate_list(x):
if isinstance(x, (Var, VarData)):
if not x.is_indexed():
return ComponentSet([x])
ans = ComponentSet()
for j in x.index_set():
ans.add(x[j])
return ans
elif hasattr(x, '__iter__'):
ans = ComponentSet()
for i in x:
ans.update(vars_to_eliminate_list(i))
return ans
else:
raise ValueError("Expected Var or list of Vars.\n\tReceived %s" % type(x))


def gcd(a, b):
while b != 0:
a, b = b, a % b
Expand Down Expand Up @@ -111,7 +95,7 @@ class Fourier_Motzkin_Elimination_Transformation(Transformation):
'vars_to_eliminate',
ConfigValue(
default=None,
domain=vars_to_eliminate_list,
domain=var_component_set,
description="Continuous variable or list of continuous variables to "
"project out of the model",
doc="""
Expand Down
1 change: 1 addition & 0 deletions pyomo/core/plugins/transform/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@
add_slack_vars,
scaling,
logical_to_linear,
lp_dual,
)
261 changes: 261 additions & 0 deletions pyomo/core/plugins/transform/lp_dual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
# ___________________________________________________________________________
#
# 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.autoslots import AutoSlots
from pyomo.common.collections import ComponentMap
from pyomo.common.config import ConfigDict, ConfigValue
from pyomo.common.errors import MouseTrap
from pyomo.common.dependencies import scipy
from pyomo.core import (
ConcreteModel,
Block,
Var,
Constraint,
Objective,
TransformationFactory,
NonNegativeReals,
NonPositiveReals,
maximize,
minimize,
Reals,
)
from pyomo.opt import WriterFactory
from pyomo.repn.standard_repn import isclose_const
from pyomo.util.var_list_domain import var_component_set


class _LPDualData(AutoSlots.Mixin):
__slots__ = ('primal_var', 'dual_var', 'primal_constraint', 'dual_constraint')

def __init__(self):
self.primal_var = {}
self.dual_var = {}
self.primal_constraint = ComponentMap()
self.dual_constraint = ComponentMap()


Block.register_private_data_initializer(_LPDualData)


@TransformationFactory.register(
'core.lp_dual', 'Generate the linear programming dual of the given model'
)
class LinearProgrammingDual(object):
CONFIG = ConfigDict("core.lp_dual")
CONFIG.declare(
'parameterize_wrt',
ConfigValue(
default=None,
domain=var_component_set,
description="Vars to treat as data for the purposes of taking the dual",
doc="""
Optional list of Vars to be treated as data while taking the LP dual.
For example, if this is the dual of the inner problem in a multilevel
optimization problem, then the outer problem's Vars would be specified
in this list since they are not variables from the perspective of the
inner problem.
""",
),
)

def apply_to(self, model, **options):
raise MouseTrap(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think MouseTrap is the right error here. NotImplementedError would make more sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MouseTrap inherits from (and is a special case of) NotImplementedError

"The 'core.lp_dual' transformation does not currently implement "
"apply_to since it is a bit ambiguous what it means to take a dual "
"in place. Please use 'create_using' and do what you wish with the "
"returned model."
)

def create_using(self, model, ostream=None, **kwds):
"""Take linear programming dual of a model
Returns
-------
ConcreteModel containing linear programming dual
Parameters
----------
model: ConcreteModel
The concrete Pyomo model to take the dual of
ostream: None
This is provided for API compatibility with other writers
and is ignored here.
"""
config = self.CONFIG(kwds.pop('options', {}))
config.set_value(kwds)

if config.parameterize_wrt is None:
std_form = WriterFactory('compile_standard_form').write(
model, mixed_form=True, set_sense=None
)
else:
std_form = WriterFactory('parameterized_standard_form_compiler').write(
model, wrt=config.parameterize_wrt, mixed_form=True, set_sense=None
)
return self._take_dual(model, std_form)

def _take_dual(self, model, std_form):
if len(std_form.objectives) != 1:
raise ValueError(
"Model '%s' has no objective or multiple active objectives. Cannot "
"take dual with more than one objective!" % model.name
)
primal_sense = std_form.objectives[0].sense

dual = ConcreteModel(name="%s dual" % model.name)
# This is a csc matrix, so we'll skip transposing and just work off
# of the columns
A = std_form.A
c = std_form.c.todense()
dual_rows = range(A.shape[1])
dual_cols = range(A.shape[0])
dual.x = Var(dual_cols, domain=NonNegativeReals)
trans_info = dual.private_data()
for j, (primal_cons, ineq) in enumerate(std_form.rows):
# maximize is -1 and minimize is +1 and ineq is +1 for <= and -1 for
# >=, so we need to change domain to NonPositiveReals if the product
# of these is +1.
if primal_sense * ineq == 1:
dual.x[j].domain = NonPositiveReals
elif ineq == 0:
# equality
dual.x[j].domain = Reals
trans_info.primal_constraint[dual.x[j]] = primal_cons
trans_info.dual_var[primal_cons] = dual.x[j]

dual.constraints = Constraint(dual_rows)
for i, primal in enumerate(std_form.columns):
lhs = 0
for j in range(A.indptr[i], A.indptr[i + 1]):
coef = A.data[j]
primal_row = A.indices[j]
lhs += coef * dual.x[primal_row]

if primal.domain is Reals:
dual.constraints[i] = lhs == c[0, i]
elif primal_sense is minimize:
if primal.domain is NonNegativeReals:
dual.constraints[i] = lhs <= c[0, i]
else: # primal.domain is NonPositiveReals
dual.constraints[i] = lhs >= c[0, i]
else:
if primal.domain is NonNegativeReals:
dual.constraints[i] = lhs >= c[0, i]
else: # primal.domain is NonPositiveReals
dual.constraints[i] = lhs <= c[0, i]
trans_info.dual_constraint[primal] = dual.constraints[i]
trans_info.primal_var[dual.constraints[i]] = primal

dual.obj = Objective(
expr=sum(std_form.rhs[j] * dual.x[j] for j in dual_cols),
sense=-primal_sense,
)

return dual

def get_primal_constraint(self, model, dual_var):
"""Return the primal constraint corresponding to 'dual_var'
Returns
-------
Constraint
Parameters
----------
model: ConcreteModel
A dual model returned from the 'core.lp_dual' transformation
dual_var: Var
A dual variable on 'model'
"""
primal_constraint = model.private_data().primal_constraint
if dual_var in primal_constraint:
return primal_constraint[dual_var]
else:
raise ValueError(
"It does not appear that Var '%s' is a dual variable on model '%s'"
% (dual_var.name, model.name)
)

def get_dual_constraint(self, model, primal_var):
"""Return the dual constraint corresponding to 'primal_var'
Returns
-------
Constraint
Parameters
----------
model: ConcreteModel
A primal model passed as an argument to the 'core.lp_dual' transformation
primal_var: Var
A primal variable on 'model'
"""
dual_constraint = model.private_data().dual_constraint
if primal_var in dual_constraint:
return dual_constraint[primal_var]
else:
raise ValueError(
"It does not appear that Var '%s' is a primal variable on model '%s'"
% (primal_var.name, model.name)
)

def get_primal_var(self, model, dual_constraint):
"""Return the primal variable corresponding to 'dual_constraint'
Returns
-------
Var
Parameters
----------
model: ConcreteModel
A dual model returned from the 'core.lp_dual' transformation
dual_constraint: Constraint
A constraint on 'model'
"""
primal_var = model.private_data().primal_var
if dual_constraint in primal_var:
return primal_var[dual_constraint]
else:
raise ValueError(
"It does not appear that Constraint '%s' is a dual constraint on "
"model '%s'" % (dual_constraint.name, model.name)
)

def get_dual_var(self, model, primal_constraint):
"""Return the dual variable corresponding to 'primal_constraint'
Returns
-------
Var
Parameters
----------
model: ConcreteModel
A primal model passed as an argument to the 'core.lp_dual' transformation
primal_constraint: Constraint
A constraint on 'model'
"""
dual_var = model.private_data().dual_var
if primal_constraint in dual_var:
return dual_var[primal_constraint]
else:
raise ValueError(
"It does not appear that Constraint '%s' is a primal constraint on "
"model '%s'" % (primal_constraint.name, model.name)
)
Comment on lines +167 to +261
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in all of these is basically exactly the same minus the thing that you are getting. You could abstract this by doing something like:

def _get_corresponding_entity(self, model, entity, entity_type, mapping):
    """Return the corresponding entity based on the provided mapping.
    
    Parameters
    ----------
    model: ConcreteModel
        The model containing the mappings.
    entity: Var or Constraint
        The entity for which we want to find the corresponding entity.
    entity_type: str
        A string indicating whether the entity is 'dual' or 'primal'.
    mapping: dict
        The mapping to look up the corresponding entity.
    
    Returns
    -------
    Var or Constraint
        The corresponding entity.
    
    Raises
    ------
    ValueError
        If the entity is not found in the mapping.
    """
    if entity in mapping:
        return mapping[entity]
    else:
        raise ValueError(
            "It does not appear that %s '%s' is a %s entity on model '%s'"
            % (entity_type.capitalize(), entity.name, 'dual' if entity_type == 'primal' else 'primal', model.name)
        )

def get_primal_constraint(self, model, dual_var):
    """Return the primal constraint corresponding to 'dual_var'."""
    primal_constraint = model.private_data().primal_constraint
    return self._get_corresponding_entity(model, dual_var, 'primal', primal_constraint)

Loading
Loading