[Issue 1224] Introduce one-sided constraints to the LP interface. - #299
[Issue 1224] Introduce one-sided constraints to the LP interface.#299gutodelazeri wants to merge 4 commits into
Conversation
| enum class Sense { | ||
| GE, // ax >= b | ||
| LE, // ax <= b | ||
| EQ // ax = b |
There was a problem hiding this comment.
I think those should be spelled out. GREATER_EQUAL, LESS_EQUAL, EQUAL.
| void set_right_hand_side(double rhs) { | ||
| right_hand_side = rhs; | ||
| } | ||
| void set_upper_bound(double ub) { |
There was a problem hiding this comment.
I think the interface with set_upper_bound and set_lower_bound is beneficial, because it allows me to not think about the way you write down your constraints.
Is it x+y>=1 or 1<=x+y, they are the same semantics.
if i have the expression x+y I would enjoy to just state a semantic wish "please restrict this with an upper bound of 1", and you take care of writing it down.
I don't think it is very useful to modify the sense or the rhs on its own. When would you ever do this?
There was a problem hiding this comment.
We discussed this in person and decided to keep the functions as they are. The main reasons are
- We do not have an expression class, so we cannot represent
x+yby itself as in the example above. Without a class representing expressions, and only a class representing a one-sided constraint, the functions imply a functionality that we do not support. - with two set-bound functions, it is possible to create a trivially unsolvable constraint
1 <= EXPRESSION <= 0which cannot be represented in CPLEX. This pull request gets rid of the complexity that was necessary to deal with this situation and having both set-bounds functions would re-introduce it.
FlorianPommerening
left a comment
There was a problem hiding this comment.
Looks good to me. I left comments but mostly they are about superficial things (renaming variables, etc.) that do not require re-running experiments. The only comment that could affect performance is about removing a set_sense in the delete-relaxation constraints.
| } | ||
| } | ||
|
|
||
| static char constraint_sense_to_cplex_sense(lp::Sense constraint_sense) { |
There was a problem hiding this comment.
Do we need lp:: here? We should be inside the scope of the namespace already.
There was a problem hiding this comment.
Also, to be consistent with LPObjectiveSense, maybe we should rename Sense to LPConstraintSense or ConstraintSense and the function model_sense_to_cplex_sense to objective_sense_to_cplex_sense.
| cerr << "Unsupported constraint sense: " | ||
| << static_cast<int>(constraint_sense) << endl; | ||
| utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); |
There was a problem hiding this comment.
| cerr << "Unsupported constraint sense: " | |
| << static_cast<int>(constraint_sense) << endl; | |
| utils::exit_with(utils::ExitCode::SEARCH_CRITICAL_ERROR); | |
| ABORT("Unsupported constraint sense: " + | |
| static_cast<int>(constraint_sense)); |
We usually use ABORT for "this should not be possible", i.e., errors targeted to developers rather than users.
| void set_right_hand_side(double rhs) { | ||
| right_hand_side = rhs; | ||
| } | ||
| void set_upper_bound(double ub) { |
There was a problem hiding this comment.
We discussed this in person and decided to keep the functions as they are. The main reasons are
- We do not have an expression class, so we cannot represent
x+yby itself as in the example above. Without a class representing expressions, and only a class representing a one-sided constraint, the functions imply a functionality that we do not support. - with two set-bound functions, it is possible to create a trivially unsolvable constraint
1 <= EXPRESSION <= 0which cannot be represented in CPLEX. This pull request gets rid of the complexity that was necessary to deal with this situation and having both set-bounds functions would re-introduce it.
| for (const LPConstraint &constraint : constraints) { | ||
| num_nonzeros += constraint.get_coefficients().size(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Was this intentional? I liked the newline there to separate the block dealing with nonzeros from the block dealing with constraints. If you prefer otherwise, that is also fine with me.
| const double b = constraint.get_right_hand_side(); | ||
| const lp::Sense s = constraint.get_sense(); |
There was a problem hiding this comment.
| const double b = constraint.get_right_hand_side(); | |
| const lp::Sense s = constraint.get_sense(); | |
| const double rhs = constraint.get_right_hand_side(); | |
| const lp::Sense sense = constraint.get_sense(); |
| con_id = get_constraint_id(f); | ||
| lp_solver.set_constraint_lower_bound(con_id, 0); | ||
| lp_solver.set_constraint_upper_bound(con_id, 0); | ||
| lp_solver.set_constraint_sense(con_id, lp::Sense::EQ); |
There was a problem hiding this comment.
Do we have to set the sense here (and in line 548)? If all these constraints start as equality constraints and are never changed, then not changing the sense here might be ok and more efficient.
| missing_solver = "SoPlex"; | ||
| #endif | ||
| break; | ||
|
|
There was a problem hiding this comment.
There is no newline before the other cases. I'd not add one here.
| } else if (lower_bound == -infinity) { | ||
| stream << " <= infinity"; | ||
| } | ||
| stream << get_sense() << get_right_hand_side(); |
There was a problem hiding this comment.
I think get_right_hand_side() could still return +/- infinity as a solver-internal constant, which would need to be translated to strings infinity and -infinity.
| rhs.resize(num_rows); | ||
| if (dense_range_values) { | ||
| range_values.resize(num_rows, 0); | ||
| if (num_rows > 0) { |
There was a problem hiding this comment.
Does this if make a difference here?
| sense[row_index] = sense_value; | ||
| double rhs_value = constraint.get_right_hand_side(); | ||
| lp::Sense constraint_sense = constraint.get_sense(); | ||
| char cplex_sense = constraint_sense_to_cplex_sense(constraint_sense); |
There was a problem hiding this comment.
if the variable is only needed in the next line, I'd directly assign to the array without a temporary variable.
This PR changes the LP interface from two-sided constraints
to one-sided constraints and equalities:
LPConstraintnow stores aSense(LE,GE, orEQ) and one right-hand side.This simplifies LP backends. It is also preparatory work for adding further LP solvers in issue 1199 (https://issues.fast-downward.org/issue1199).
Main changes
LPConstraint's lower and upper bounds with a constraint sense and right-hand side.set_constraint_lower_bound()andset_constraint_upper_bound()withset_constraint_rhs()andset_constraint_sense().Important notes
The old interface contained two places where two-sided constraints were created, but one side was redundant:
f_qandf_{p,a}. In the paper, the authors do not mention an upper bound.The new interface does not add these redundant constraints. The generated LP models, however, are equivalent, i.e., their feasible region and objective function remain unchanged.
Validation
We compared the new interface with baseline Fast Downward on the full optimal benchmark suite using CPLEX and SoPlex across 11 solver/configuration combinations.
Overall:
We separately tested restoring the redundant Rankooh-Rintanen upper bound as an additional one-sided constraint. This did not recover the behavior of baseline Fast Downward and slightly reduced coverage, so the redundant constraint is not included in this PR.
Experiment details and discussion:
https://issues.fast-downward.org/issue1224