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

Merged
merged 81 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
81 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
e4f3193
Implementing Miranda's suggestion for the API mapping generalization
emma58 Nov 6, 2024
7539e81
black
emma58 Nov 6, 2024
7306e24
Merge branch 'lp-dual' of github.com:emma58/pyomo into lp-dual
emma58 Nov 6, 2024
cfb9701
Citing the scipy algorithms I'm reimplementing in docstrings
emma58 Nov 6, 2024
317802c
Taking John's suggestions for comments about scipy beauty
emma58 Nov 6, 2024
52e7207
Merge branch 'main' into lp-dual
emma58 Nov 8, 2024
7a296c8
Renaming the parameterized standard form compiler
emma58 Nov 8, 2024
669e6d5
Generalizing the VarData list domain, fixing the error for multiple a…
emma58 Nov 10, 2024
4b8ec54
Emma learns numpy--making c vector a vector...
emma58 Nov 10, 2024
9e07057
Black
emma58 Nov 10, 2024
1c48982
Reverting the generalization of the dual-to-primal mapping API
emma58 Nov 10, 2024
f3e4acd
NFC: fixing comment typo
emma58 Nov 10, 2024
25749b0
Switching apply_to error to NotImplementedError
emma58 Nov 10, 2024
d9c2a73
Adding a test for sum_duplicates
emma58 Nov 11, 2024
fc5d5dd
black
emma58 Nov 11, 2024
2a5e1aa
Adding some input validation and a test for it
emma58 Nov 11, 2024
42ab075
black
emma58 Nov 11, 2024
b694bbe
Changing ComponentDataList to ComponentDataSet and taking most of Joh…
emma58 Nov 11, 2024
ac49dda
black
emma58 Nov 11, 2024
d0b813f
NFC: Updating docstring for ComponentDataSet
emma58 Nov 12, 2024
729631d
Adding tests for the ComponentDataSet config domain
emma58 Nov 12, 2024
51c57d2
Black
emma58 Nov 12, 2024
d6931c4
Merge branch 'main' into lp-dual
emma58 Nov 12, 2024
5ec2dc7
NFC: Fixing a typo in the ComponentDataSet docstring
emma58 Nov 12, 2024
5e8913f
Update ComponentDataSet for efficiency, more permissible inputs
jsiirola Nov 13, 2024
9e6bf2e
Merge branch 'main' into lp-dual
jsiirola Nov 13, 2024
9f9c389
Merge branch 'main' into lp-dual
jsiirola Nov 14, 2024
aa5e6d0
Merge branch 'main' into lp-dual
blnicho Nov 14, 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.config_domains import ComponentDataSet

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=ComponentDataSet(Var),
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,
)
260 changes: 260 additions & 0 deletions pyomo/core/plugins/transform/lp_dual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# ___________________________________________________________________________
#
# 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.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.config_domains import ComponentDataSet


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=ComponentDataSet(Var),
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 NotImplementedError(
"The 'core.lp_dual' transformation does not implement "
"apply_to since it is 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('compile_parameterized_standard_form').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. Can "
"only take dual with exactly one active 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().ravel()
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[i]
elif primal_sense is minimize:
if primal.domain is NonNegativeReals:
dual.constraints[i] = lhs <= c[i]
else: # primal.domain is NonPositiveReals
dual.constraints[i] = lhs >= c[i]
else:
if primal.domain is NonNegativeReals:
dual.constraints[i] = lhs >= c[i]
else: # primal.domain is NonPositiveReals
dual.constraints[i] = lhs <= c[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)
)
emma58 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading