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

Conversation

emma58
Copy link
Contributor

@emma58 emma58 commented Nov 5, 2024

Fixes the complaints of several reasonable people

Summary/Motivation:

This PR adds a transformation in core to take the dual of a linear program. For the purposes of supporting bilevel programming, it also allows for this dual to be "parameterized" by certain variables--that is, the user can specify a list of Vars that are treated as data for the purposes of taking the dual (e.g., if they are the outer variables of a bilevel program with an LP inner problem).

Changes proposed in this PR:

  • Adds core.lp_dual transformation.
  • Adds a parameterized standard form writer to get the A, b, and c matrices for taking the dual.
  • Adds a very vanilla Python implementation of CSR and CSC matrices for use in the above--we can't use scipy because we need to allow Pyomo expressions in sparse matrix representations

Legal Acknowledgement

By contributing to this software project, I have read the contribution guide and agree to the following terms and conditions for my contribution:

  1. I agree my contributions are submitted under the BSD license.
  2. I represent I am authorized to make the contributions and grant the license. If my employer has rights to intellectual property that includes these contributions, I represent that I have received permission to make contributions and grant the required license on behalf of that employer.

…that assume that the data is numeric. Creating my own csr and csc classes that are almost complete
…use the correct converstion to a CSC matrix depending on the circumstances, testing the conversion to nonnegative vars
)

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

Comment on lines +167 to +261
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)
)
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)

Comment on lines +117 to +120
# we leave off the last entry (the number of nonzeros) because we are
# going to do the cute scipy thing and it will get 'added' at the end
# when we shift right (by which I mean it will conveniently already be
# there)
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 quite understand what this comment is attached to - the lines above or below?

num_non_zeros = 0
col_end = 0
for i in range(ncols):
jj = col_end
Copy link
Contributor

Choose a reason for hiding this comment

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

From a math/matrix perspective, I get this; from a SWE perspective, I don't. Not asking for a change - just making a comment about jj in software.

Comment on lines +16 to +34
def var_component_set(x):
"""
For domain validation in ConfigDicts: Takes singletone or iterable argument 'x'
of Vars and converts it to a ComponentSet of Vars.
"""
if hasattr(x, 'ctype') and x.ctype is Var:
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(var_component_set(i))
return ans
else:
raise ValueError("Expected Var or iterable of Vars.\n\tReceived %s" % type(x))
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this make more sense in pyomo/common/config.py? There are other domain validators in there.

Copy link
Member

Choose a reason for hiding this comment

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

Possibly, but we require that pyomo.common NOT import from any other part of Pyomo, and this validator needs to know about Var. This just came up in #3384 as well: we should decide on a good place for "Pyomo-aware" domain validators.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants