-
Notifications
You must be signed in to change notification settings - Fork 526
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
Add branching priorities to CPLEXSHELL #1300
Conversation
242ff65
to
9bbba62
Compare
- Include `branch_priority` and `branch_direction` attributes of variable data - Read off these attributes as part of the CPLEXSHELL solve and write them to a CPLEX .ord file - Update the CPLEX OPL script to read these .ord file should it exist
- End-to-end tests of `opt.solve()` using branching priorities with CPLEXSHELL
- Declare `priority` and `direction` suffixes on the model instead of attributes on the variable data in order to represent branching decisions
9bbba62
to
5e44956
Compare
Codecov Report
@@ Coverage Diff @@
## master #1300 +/- ##
=========================================
+ Coverage 68.57% 71.5% +2.92%
=========================================
Files 534 540 +6
Lines 81026 87389 +6363
=========================================
+ Hits 55567 62486 +6919
+ Misses 25459 24903 -556
Continue to review full report at Codecov.
|
- Looks like there's some circular imports failing specifically for Py2.7 - Follow the same (hacky) local import design chosen in 0935e75
- Update tests to avoid usage of "magic numbers" and reference `BranchDirection` where possible
This should be ready for review as all the checks are passing now. GitHub is giving me a few suggestions (@ghackebeil, @jsiirola, @whart222) though I'm not sure who would be best placed so whenever one of you is free! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation looks good to me. My only real concern is whether or not we want to support this type of functionality through a file-based interface. Cplex has many, many options and configurations, and if we were to add support for all of them, the interface would be terribly complex. On the other hand, we could focus our effort on improving the direct/persistent interface, where it would be very easy to support almost any Cplex option/configuration. Thoughts @jsiirola @carldlaird @jwatsonnm @ghackebeil @DLWoodruff ?
from pyomo.solvers.plugins.converter.model import PyomoMIPConverter # register the `ProblemConverterFactory` | ||
from pyomo.repn.plugins.cpxlp import ProblemWriter_cpxlp # register the `WriterFactory` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead if explicitly importing the plugins here, I think it would be preferable to just
import pyomo.environ
at the top of the module (which will ensure that standard plugins are registered.
"* ENCODING=ISO-8859-1\nNAME Priority Order\n %s x1 %d\nENDATA\n" | ||
% (ORDFileSchema._direction_to_str(direction_val), priority_val), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would prefer this string to be hard-coded (not generated with %s
placeholders). Doing so will ensure that this also tests that the ORDFileSchema._direction_to_str
is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I've updated this and the below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below got updated, but I was thinking something closer to this here:
self.assertEqual(
priorities_file,
"* ENCODING=ISO-8859-1\n"
"NAME Priority Order\n"
" DN x1 10\n"
"ENDATA\n")
"* ENCODING=ISO-8859-1\nNAME Priority Order\n x1 %d\nENDATA\n" | ||
% (priority_val,), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above: I would recommend hard-coding the reference string.
@@ -44,3 +50,12 @@ def __enter__(self): | |||
def __exit__(self, t, v, traceback): | |||
pass | |||
|
|||
|
|||
class BranchDirection(object): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Open question: is this the best place for this enum-like object?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wondered the same thing, but I'm not sure of a better place for it. Gurobi has the same options (up, down, and default).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this was the only place general enough that I could think of as there's nothing else as suitable. We could always introduce a new module for this though?
Thanks @michaelbynum. I am certainly up for improving the direct/persistent interface as well and would be happy to raise a separate PR to implement this functionality for that. As it happens, we were previously using With regards to supporting advanced functionality in |
@michaelbynum, @jsiirola, back to you. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found some more questions (all in the testing). If you have a few minutes, it would be great if you could clean the testing up a little more. But, I am not going to hold up the PR for it. @michaelbynum?
|
||
def get_mock_model(self): | ||
model = ConcreteModel() | ||
model.x = Var(within=Binary) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I missed this, but we should add 2 more things:
- an indexed variable
m.y = Var([1,2])
to this test - a new test that uses a copy of this model using the kernel API (I was going to suggest a change in the actual implementation before I realized it was likely to break things for Kernel. It would be very good to test that here)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
an indexed variable m.y = Var([1,2]) to this test
Thanks, added.
Yeah, it sure didn't work for kernel
🤦♂
I'm not too familiar with that API so I've had to add special checks as to whether the variable is indexed (see var_is_indexed()
var_iter()
). Any alternatives to make that neater would be great!
"* ENCODING=ISO-8859-1\nNAME Priority Order\n %s x1 %d\nENDATA\n" | ||
% (ORDFileSchema._direction_to_str(direction_val), priority_val), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Below got updated, but I was thinking something closer to this here:
self.assertEqual(
priorities_file,
"* ENCODING=ISO-8859-1\n"
"NAME Priority Order\n"
" DN x1 10\n"
"ENDATA\n")
) | ||
|
||
|
||
class CPLEXShellSolvePrioritiesFile(unittest.TestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this a separate class from the previous priorities tester?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The former class is a unit test specifically on the writing of priorities via _write_priorities_file()
. The latter is an integration test on the end-to-end application of priorities via the Suffix
through a solve()
. I've added docs to that effect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, this is looking very good. Thank you!
I think I have one last suggestion for cleaning up the Var/Kernel iteration. If that works, then we should merge.
@@ -268,14 +275,14 @@ def _convert_priorities_to_rows(self, instance, priorities, directions): | |||
|
|||
var_direction = directions.get(var, BranchDirection.default) | |||
|
|||
if not var.is_indexed(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the easiest general solution is:
if hasattr(var, 'values'):
# catches Var, IndexedVar, and kernel's variable_dict
var_iter = var.values()
elif hasattr(var, '__iter__'):
# catches list and kernel's variable_list and variable_tuple
var_iter = iter(var)
else:
# kernel's variable
var_iter = iter((var,))
for v in var_iter:
if id(v) not in byObject:
continue
v_direction = directions.get(v, var_direction)
rows.append((byObject[id(v)], priority, v_direction))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @jsiirola! I've added this logic as a general-purpose helper in pyomo.util.components
however let me know if you'd rather this logic is kept private to this method or this isn't the best place for it. (To be honest, I'm surprised this hasn't come up before!)
- Add iterator for looping over the data objects of either a `base` or `kernel` component
OSX builds are failing, but it does not have anything to do with this PR. I just created another PR which will hopefully fix the problem. Then, we can get this merged. |
def iter_component(obj): | ||
""" | ||
Yield "child" objects from a component that is defined with either the `base` or `kernel` APIs. | ||
If the component is not indexed, it returns itself. | ||
|
||
Parameters | ||
---------- | ||
obj : ComponentType | ||
eg. `TupleContainer`, `ListContainer`, `DictContainer`, `IndexedComponent`, or `Component` | ||
|
||
Returns | ||
------- | ||
Iterator[ComponentType] : Iterator of the component data objects. | ||
""" | ||
try: | ||
# catches `IndexedComponent`, and kernel's `_dict` | ||
return iter(obj.values()) | ||
except AttributeError: | ||
pass | ||
|
||
try: | ||
# catches list and kernel's `_list` and `_tuple` | ||
return iter(obj) | ||
except TypeError: | ||
return iter((obj,)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ruaridhw Thanks for adding this. This will be very useful in other places too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks good to me. Like I said, I am working on getting the osx builds fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One fix for Py2/3 compatibility and we will merge.
pyomo/util/tests/test_components.py
Outdated
@@ -7,10 +7,13 @@ | |||
# rights in this software. | |||
# This software is distributed under the 3-clause BSD License. | |||
# ___________________________________________________________________________ | |||
from itertools import zip_longest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import error can be fixed by:
from six.moves import zip_longest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, updated.
Fixes #1299
Summary/Motivation:
To be able to use the branching priority capability of commercial solvers when solving MIP problems
Changes proposed in this PR:
Legal Acknowledgement
By contributing to this software project, I agree to the following terms and conditions for my contribution: