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

Add branching priorities to CPLEXSHELL #1300

Merged
merged 19 commits into from
Mar 17, 2020

Conversation

ruaridhw
Copy link
Contributor

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:

  • Add branching priority / direction data to variables
  • Add branching priority capability to the CPLEXSHELL interface

Legal Acknowledgement

By contributing to this software project, I 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.

@ruaridhw ruaridhw force-pushed the cplex_branching_priorities branch from 242ff65 to 9bbba62 Compare February 20, 2020 11:31
- 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
@ruaridhw ruaridhw force-pushed the cplex_branching_priorities branch from 9bbba62 to 5e44956 Compare February 20, 2020 15:24
@codecov
Copy link

codecov bot commented Feb 21, 2020

Codecov Report

Merging #1300 into master will increase coverage by 2.92%.
The diff coverage is 96.38%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
pyomo/opt/base/problem.py 94.11% <100%> (+2.45%) ⬆️
pyomo/util/components.py 100% <100%> (ø) ⬆️
pyomo/solvers/plugins/solvers/CPLEX.py 88.7% <95.52%> (+3.82%) ⬆️
pyomo/solvers/plugins/solvers/gurobi_direct.py 60.24% <0%> (-10.76%) ⬇️
examples/gdp/jobshop.py 91.3% <0%> (-8.7%) ⬇️
pyomo/pysp/plugins/csvsolutionwriter.py 95.34% <0%> (-1.63%) ⬇️
pyomo/contrib/pynumero/sparse/intrinsic.py 86.66% <0%> (-1.54%) ⬇️
pyomo/pysp/phinit.py 54.38% <0%> (-1.18%) ⬇️
pyomo/contrib/gdpopt/iterate.py 77.41% <0%> (-0.77%) ⬇️
pyomo/opt/testing/pyunit.py 28.94% <0%> (-0.29%) ⬇️
... and 74 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update bf7daa3...456cb86. Read the comment docs.

- 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
@ruaridhw
Copy link
Contributor Author

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!

Copy link
Contributor

@michaelbynum michaelbynum left a 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 ?

Comment on lines 65 to 66
from pyomo.solvers.plugins.converter.model import PyomoMIPConverter # register the `ProblemConverterFactory`
from pyomo.repn.plugins.cpxlp import ProblemWriter_cpxlp # register the `WriterFactory`
Copy link
Member

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.

Comment on lines 131 to 132
"* ENCODING=ISO-8859-1\nNAME Priority Order\n %s x1 %d\nENDATA\n"
% (ORDFileSchema._direction_to_str(direction_val), priority_val),
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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")

Comment on lines 160 to 161
"* ENCODING=ISO-8859-1\nNAME Priority Order\n x1 %d\nENDATA\n"
% (priority_val,),
Copy link
Member

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):
Copy link
Member

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?

Copy link
Contributor

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).

Copy link
Contributor Author

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?

@ruaridhw
Copy link
Contributor Author

ruaridhw commented Mar 4, 2020

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 CPLEXPersistent for this advanced functionality and found it to be a little unreliable (see known issues such as #1244, #1169) hence the (hopefully temporary) move back to CPLEXSHELL and this PR.

With regards to supporting advanced functionality in CPLEXSHELL, from what I can see, there are only a handful of file-based interfaces that pyomo doesn't already support. Personally, the only other input format that would be much use would be "ANN" for model annotations. I understand that all other settings and parameters are already exposed by pyomo via solver.options.

@michaelbynum
Copy link
Contributor

@ruaridhw Good points. I am working on improving the direct/persistent solvers separately. This is a good solution for now. If you can address @jsiirola 's comments, we will get this merged.

@ruaridhw
Copy link
Contributor Author

ruaridhw commented Mar 12, 2020

@michaelbynum, @jsiirola, back to you.

Copy link
Member

@jsiirola jsiirola left a 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)
Copy link
Member

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)

Copy link
Contributor Author

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!

Comment on lines 131 to 132
"* ENCODING=ISO-8859-1\nNAME Priority Order\n %s x1 %d\nENDATA\n"
% (ORDFileSchema._direction_to_str(direction_val), priority_val),
Copy link
Member

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):
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Member

@jsiirola jsiirola left a 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():
Copy link
Member

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))

Copy link
Contributor Author

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
@ruaridhw ruaridhw requested a review from jsiirola March 16, 2020 11:53
@michaelbynum
Copy link
Contributor

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.

Comment on lines +58 to +82
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,))
Copy link
Contributor

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.

Copy link
Contributor

@michaelbynum michaelbynum left a 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.

Copy link
Member

@jsiirola jsiirola left a 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.

@@ -7,10 +7,13 @@
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
from itertools import zip_longest
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, updated.

@jsiirola jsiirola merged commit c5ca6e1 into Pyomo:master Mar 17, 2020
@ruaridhw ruaridhw deleted the cplex_branching_priorities branch March 17, 2020 22:37
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.

Issue Branching Priorities
5 participants