-
Notifications
You must be signed in to change notification settings - Fork 577
Implicit constraint #3870
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
base: main
Are you sure you want to change the base?
Implicit constraint #3870
Changes from 17 commits
0b1fe30
92265d7
cc4b693
f67e0c4
0a8f340
952d2ff
85253e9
e9e7007
e2628d6
8970a57
1cb696c
e77845b
8f3cce2
e694391
01818ca
66eab7f
2a3fd7a
f3bb8fd
1fa3ce6
55b6434
01df76b
48a4a5d
09654f5
fa781b2
974ce8a
9936edf
0d4184a
4cac8da
0d426fe
c93e330
9cc2bcb
b831f45
5e7ef58
5eb7d47
e19e53f
e78da85
e15dfc5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| # ___________________________________________________________________________ | ||
| # | ||
| # Pyomo: Python Optimization Modeling Objects | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please check / update the copyright statement (this is last year's). |
||
| # Copyright (c) 2008-2025 | ||
| # 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. | ||
| # ___________________________________________________________________________ | ||
| # | ||
| # Additional contributions Copyright (c) 2026 OLI Systems, Inc. | ||
| # ___________________________________________________________________________ | ||
|
|
||
| import pyomo.common.unittest as unittest | ||
| import pyomo.environ as pyo | ||
|
|
||
| from pyomo.contrib.incidence_analysis import IncidenceGraphInterface | ||
| from pyomo.contrib.pynumero.interfaces.external_grey_box import ExternalGreyBoxBlock | ||
| import pyomo.contrib.pynumero.interfaces.tests.external_grey_box_models as ex_models | ||
|
|
||
|
|
||
| class TestExternalGreyBoxAsNLP(unittest.TestCase): | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| def test_pressure_drop_single_output(self): | ||
| m = pyo.ConcreteModel() | ||
| m.egb = ExternalGreyBoxBlock() | ||
| m.egb.set_external_model( | ||
| ex_models.PressureDropSingleOutput(), build_implicit_constraint_objects=True | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hopefully I will leave a comment later where this is actually implemented, but I would prefer not to have this option. If we are going to add EGB constraint objects, they should be built by default.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we remove the option entirely, or just make
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I say just remove the option entirely.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| ) | ||
|
|
||
| igraph = IncidenceGraphInterface(m, include_inequality=False) | ||
| var_dm_partition, con_dm_partition = igraph.dulmage_mendelsohn() | ||
|
|
||
| uc_var = var_dm_partition.unmatched + var_dm_partition.underconstrained | ||
| uc_con = con_dm_partition.underconstrained | ||
| oc_var = var_dm_partition.overconstrained | ||
| oc_con = con_dm_partition.overconstrained + con_dm_partition.unmatched | ||
|
Comment on lines
+36
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No action necessary, but this is reminding me that it would be nice to have an easier way to access the UC and OC sets without needing to know to add the unmatched components. |
||
|
|
||
| assert len(uc_var) == 4 | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| for i in uc_var: | ||
| assert i.name in [ | ||
| "egb.inputs[Pin]", | ||
| "egb.inputs[c]", | ||
| "egb.inputs[F]", | ||
| "egb.outputs[Pout]", | ||
| ] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| assert len(uc_con) == 1 | ||
| assert uc_con[0].name == "egb.Pout_constraint" | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| assert len(oc_var) == 0 | ||
| assert len(oc_con) == 0 | ||
|
|
||
| max_matching = igraph.maximum_matching() | ||
| assert len(max_matching) == 1 | ||
| for k, v in max_matching.items(): | ||
| assert k.name == "egb.Pout_constraint" | ||
| assert v.name == "egb.outputs[Pout]" | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
|
|
||
| con_vars, con_cons = igraph.get_connected_components() | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| assert len(con_vars) == 1 | ||
| assert len(con_cons) == 1 | ||
| assert len(con_vars[0]) == 4 | ||
| for j in con_vars[0]: | ||
| assert j.name in [ | ||
| "egb.inputs[Pin]", | ||
| "egb.inputs[c]", | ||
| "egb.inputs[F]", | ||
| "egb.outputs[Pout]", | ||
| ] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| assert len(con_cons[0]) == 1 | ||
| for j in con_cons[0]: | ||
| assert j.name in ["egb.Pout_constraint"] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Add constraints to make model square, then rebuild graph to test block triangularization | ||
| m.con1 = pyo.Constraint(expr=m.egb.inputs["Pin"] == 1) | ||
| m.con2 = pyo.Constraint(expr=m.egb.inputs["c"] == 1) | ||
| m.con3 = pyo.Constraint(expr=m.egb.inputs["F"] == 1) | ||
| igraph = IncidenceGraphInterface(m, include_inequality=False) | ||
| bt_vars, bt_cons = igraph.block_triangularize() | ||
|
|
||
| # Expect 4 decomposable sub-sets, one for each linking constraint and one for the grey box | ||
| assert len(bt_vars) == 4 | ||
| assert len(bt_cons) == 4 | ||
|
|
||
| matchings = { | ||
| "egb.inputs[Pin]": "con1", | ||
| "egb.inputs[c]": "con2", | ||
| "egb.inputs[F]": "con3", | ||
| "egb.outputs[Pout]": "egb.Pout_constraint", | ||
| } | ||
|
|
||
| for i in range(len(bt_vars)): | ||
| assert len(bt_vars[i]) == 1 | ||
| assert len(bt_cons[i]) == 1 | ||
|
|
||
| match_var = bt_vars[i][0].name | ||
| match_con = bt_cons[i][0].name | ||
|
|
||
| assert match_con == matchings[match_var] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
|
|
||
|
andrewlee94 marked this conversation as resolved.
|
||
| def test_pressure_drop_two_equalities_two_outputs(self): | ||
| m = pyo.ConcreteModel() | ||
| m.egb = ExternalGreyBoxBlock() | ||
| m.egb.set_external_model( | ||
| ex_models.PressureDropTwoEqualitiesTwoOutputs(), | ||
| build_implicit_constraint_objects=True, | ||
| ) | ||
|
|
||
| igraph = IncidenceGraphInterface(m, include_inequality=False) | ||
| var_dm_partition, con_dm_partition = igraph.dulmage_mendelsohn() | ||
|
|
||
| uc_var = var_dm_partition.unmatched + var_dm_partition.underconstrained | ||
| uc_con = con_dm_partition.underconstrained | ||
| oc_var = var_dm_partition.overconstrained | ||
| oc_con = con_dm_partition.overconstrained + con_dm_partition.unmatched | ||
|
|
||
| assert len(uc_var) == 7 | ||
| for i in uc_var: | ||
| assert i.name in [ | ||
| "egb.inputs[F]", | ||
| "egb.inputs[P1]", | ||
| "egb.inputs[P3]", | ||
| "egb.inputs[Pin]", | ||
| "egb.inputs[c]", | ||
| "egb.outputs[P2]", | ||
| "egb.outputs[Pout]", | ||
| ] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| assert len(uc_con) == 4 | ||
| for i in uc_con: | ||
| assert i.name in [ | ||
| "egb.Pout_constraint", | ||
| "egb.P2_constraint", | ||
| "egb.pdrop1", | ||
| "egb.pdrop3", | ||
| ] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| assert len(oc_var) == 0 | ||
| assert len(oc_con) == 0 | ||
|
|
||
| max_matching = igraph.maximum_matching() | ||
| assert len(max_matching) == 4 | ||
| expected_matches = { | ||
| "egb.pdrop1": "egb.inputs[Pin]", | ||
| "egb.pdrop3": "egb.inputs[c]", | ||
| "egb.P2_constraint": "egb.outputs[P2]", | ||
| "egb.Pout_constraint": "egb.outputs[Pout]", | ||
| } | ||
| for k, v in max_matching.items(): | ||
| assert v.name == expected_matches[k.name] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
|
|
||
| con_vars, con_cons = igraph.get_connected_components() | ||
| assert len(con_vars) == 1 | ||
| assert len(con_cons) == 1 | ||
|
|
||
| assert len(con_vars[0]) == 7 | ||
| for j in con_vars[0]: | ||
| assert j.name in [ | ||
| "egb.inputs[F]", | ||
| "egb.inputs[P1]", | ||
| "egb.inputs[P3]", | ||
| "egb.inputs[Pin]", | ||
| "egb.inputs[c]", | ||
| "egb.outputs[P2]", | ||
| "egb.outputs[Pout]", | ||
| ] | ||
| assert len(con_cons[0]) == 4 | ||
| for j in con_cons[0]: | ||
| assert j.name in [ | ||
| "egb.Pout_constraint", | ||
| "egb.P2_constraint", | ||
| "egb.pdrop1", | ||
| "egb.pdrop3", | ||
| ] | ||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Add constraints to make model square, then rebuild graph to test block triangularization | ||
| m.con1 = pyo.Constraint(expr=m.egb.inputs["F"] == 1) | ||
| m.con2 = pyo.Constraint(expr=m.egb.inputs["Pin"] == 1) | ||
| m.con3 = pyo.Constraint(expr=m.egb.inputs["c"] == 1) | ||
| igraph = IncidenceGraphInterface(m, include_inequality=False) | ||
| bt_vars, bt_cons = igraph.block_triangularize() | ||
|
|
||
| for i, v in enumerate(bt_vars): | ||
| print(f"\nBlock {i}\n") | ||
| for j in v: | ||
| print(j.name) | ||
| for j in bt_cons[i]: | ||
| print(j.name) | ||
|
|
||
|
andrewlee94 marked this conversation as resolved.
Outdated
|
||
| # Get 6 decomposable sub-sets | ||
| # 3 linking constraints give 3 sub-sets | ||
| # Grey box gets broken into 3 parts for some reason | ||
| assert len(bt_vars) == 6 | ||
| assert len(bt_cons) == 6 | ||
|
|
||
| for i in range(len(bt_vars)): | ||
| assert len(bt_vars[i]) == len(bt_cons[i]) | ||
|
|
||
| # Block 0 | ||
| assert len(bt_vars[0]) == 1 | ||
| assert len(bt_cons[0]) == 1 | ||
| assert bt_vars[0][0].name == "egb.inputs[F]" | ||
| assert bt_cons[0][0].name == "con1" | ||
|
|
||
| # Block 1 | ||
| assert len(bt_vars[1]) == 1 | ||
| assert len(bt_cons[1]) == 1 | ||
| assert bt_vars[1][0].name == "egb.inputs[Pin]" | ||
| assert bt_cons[1][0].name == "con2" | ||
|
|
||
| # Block 2 | ||
| assert len(bt_vars[2]) == 1 | ||
| assert len(bt_cons[2]) == 1 | ||
| assert bt_vars[2][0].name == "egb.inputs[c]" | ||
| assert bt_cons[2][0].name == "con3" | ||
|
|
||
| # Block 3 | ||
| assert len(bt_vars[3]) == 2 | ||
| assert len(bt_cons[3]) == 2 | ||
|
|
||
| for i in bt_vars[3]: | ||
| assert i.name in ["egb.inputs[P1]", "egb.inputs[P3]"] | ||
| for i in bt_cons[3]: | ||
| assert i.name in ["egb.pdrop1", "egb.pdrop3"] | ||
|
|
||
| # Block 4 | ||
| assert len(bt_vars[4]) == 1 | ||
| assert len(bt_cons[4]) == 1 | ||
| assert bt_vars[4][0].name == "egb.outputs[P2]" | ||
| assert bt_cons[4][0].name == "egb.P2_constraint" | ||
|
|
||
| # Block 5 | ||
| assert len(bt_vars[5]) == 1 | ||
| assert len(bt_cons[5]) == 1 | ||
| assert bt_vars[5][0].name == "egb.outputs[Pout]" | ||
| assert bt_cons[5][0].name == "egb.Pout_constraint" | ||
Uh oh!
There was an error while loading. Please reload this page.