-
Notifications
You must be signed in to change notification settings - Fork 175
add mip start attribute #1305
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 mip start attribute #1305
Changes from all commits
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 | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -106,6 +106,9 @@ class Variable: | |||||||||||||||||
| Value of the variable after solving. | ||||||||||||||||||
| ReducedCost : float | ||||||||||||||||||
| Reduced Cost after solving an LP problem. | ||||||||||||||||||
| MIPStart : float | ||||||||||||||||||
| Initial value (warm-start hint) for the variable. Defaults to NaN | ||||||||||||||||||
| (unset). Only used for a MIP problem. | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
||||||||||||||||||
| def __init__( | ||||||||||||||||||
|
|
@@ -124,6 +127,7 @@ def __init__( | |||||||||||||||||
| self.ReducedCost = float("nan") | ||||||||||||||||||
| self.VariableType = vtype | ||||||||||||||||||
| self.VariableName = vname | ||||||||||||||||||
| self.MIPStart = float("nan") | ||||||||||||||||||
|
|
||||||||||||||||||
| def getIndex(self): | ||||||||||||||||||
| """ | ||||||||||||||||||
|
|
@@ -199,6 +203,20 @@ def getVariableName(self): | |||||||||||||||||
| """ | ||||||||||||||||||
| return self.VariableName | ||||||||||||||||||
|
|
||||||||||||||||||
| def setMIPStart(self, val): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Sets the MIP start (initial primal solution hint) value for the | ||||||||||||||||||
| variable. Use ``float("nan")`` to unset. | ||||||||||||||||||
| """ | ||||||||||||||||||
| self.MIPStart = float(val) | ||||||||||||||||||
|
|
||||||||||||||||||
| def getMIPStart(self): | ||||||||||||||||||
| """ | ||||||||||||||||||
| Returns the MIP start (initial primal solution hint) value of the | ||||||||||||||||||
| variable. Defaults to NaN when unset. | ||||||||||||||||||
| """ | ||||||||||||||||||
| return self.MIPStart | ||||||||||||||||||
|
|
||||||||||||||||||
| def __neg__(self): | ||||||||||||||||||
| return LinearExpression([self], [-1.0], 0.0) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -1428,6 +1446,7 @@ def _to_data_model(self): | |||||||||||||||||
| self.lower_bound, self.upper_bound = np.zeros(n), np.zeros(n) | ||||||||||||||||||
| self.var_type = np.empty(n, dtype="S1") | ||||||||||||||||||
| self.var_names = [] | ||||||||||||||||||
| self.mip_start = np.empty(n, dtype=np.float64) | ||||||||||||||||||
|
|
||||||||||||||||||
| for j in range(n): | ||||||||||||||||||
| self.objective[j] = self.vars[j].getObjectiveCoefficient() | ||||||||||||||||||
|
|
@@ -1438,6 +1457,7 @@ def _to_data_model(self): | |||||||||||||||||
| if var_name == "": | ||||||||||||||||||
| var_name = "C" + str(self.vars[j].index) | ||||||||||||||||||
| self.var_names.append(var_name) | ||||||||||||||||||
| self.mip_start[j] = self.vars[j].MIPStart | ||||||||||||||||||
|
|
||||||||||||||||||
| # Initialize datamodel | ||||||||||||||||||
| dm.set_csr_constraint_matrix( | ||||||||||||||||||
|
|
@@ -1464,6 +1484,9 @@ def _to_data_model(self): | |||||||||||||||||
| dm.set_row_names(self.row_names) | ||||||||||||||||||
| dm.set_problem_name(self.Name) | ||||||||||||||||||
|
|
||||||||||||||||||
| if self.mip_start.size > 0 and not np.all(np.isnan(self.mip_start)): | ||||||||||||||||||
| dm.set_initial_primal_solution(self.mip_start) | ||||||||||||||||||
|
Comment on lines
+1487
to
+1488
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. Guard against forwarding partially-NaN MIP starts to the data model. Current condition forwards Suggested patch- if self.mip_start.size > 0 and not np.all(np.isnan(self.mip_start)):
- dm.set_initial_primal_solution(self.mip_start)
+ if self.mip_start.size > 0 and not np.all(np.isnan(self.mip_start)):
+ if np.any(np.isnan(self.mip_start)):
+ raise ValueError(
+ "MIPStart must be provided for all variables or left unset for all."
+ )
+ dm.set_initial_primal_solution(self.mip_start)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| self.model = dm | ||||||||||||||||||
|
|
||||||||||||||||||
| def update(self): | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -482,6 +482,47 @@ def test_warm_start(): | |
| ) | ||
|
|
||
|
|
||
| def test_mip_start(): | ||
| # Build a small MIP and feed it an (already-optimal) MIP start through | ||
| # the new Variable.MIPStart attribute. We verify: | ||
| # - the attribute defaults to NaN | ||
| # - setMIPStart / direct assignment both work | ||
| # - the values reach the data model via set_initial_primal_solution | ||
| # - solving still produces the optimal result | ||
| # - leaving every MIPStart as NaN does not set an initial primal sol | ||
| prob = Problem("MIP_start") | ||
| x = prob.addVariable(lb=0, ub=10, vtype=INTEGER, name="x") | ||
| y = prob.addVariable(lb=0, ub=10, vtype=INTEGER, name="y") | ||
|
|
||
| assert math.isnan(x.getMIPStart()) | ||
| assert math.isnan(y.MIPStart) | ||
|
|
||
| prob.addConstraint(x + y <= 10, name="c1") | ||
| prob.addConstraint(x - y >= 0, name="c2") | ||
| prob.setObjective(x + 2 * y, sense=MAXIMIZE) | ||
|
|
||
| x.setMIPStart(5) | ||
| y.MIPStart = 5.0 | ||
|
|
||
| assert x.getMIPStart() == 5.0 | ||
| assert y.getMIPStart() == 5.0 | ||
|
|
||
| prob._to_data_model() | ||
| initial_primal = prob.model.get_initial_primal_solution() | ||
| assert len(initial_primal) == 2 | ||
| assert initial_primal[0] == pytest.approx(5.0) | ||
| assert initial_primal[1] == pytest.approx(5.0) | ||
|
|
||
| settings = SolverSettings() | ||
| settings.set_parameter("time_limit", 5) | ||
| prob.solve(settings) | ||
|
|
||
| assert prob.Status.name == "Optimal" | ||
| assert prob.ObjValue == pytest.approx(15) | ||
| assert x.Value == pytest.approx(5) | ||
| assert y.Value == pytest.approx(5) | ||
|
|
||
|
Comment on lines
+485
to
+524
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. Add edge-case assertion for the all-NaN branch. This test validates the “set hint” path, but not the “all hints unset” path introduced by the new branch in As per coding guidelines, “Flag missing coverage for edge cases (empty, infeasible, unbounded, degenerate) when adding new code paths” and for 🤖 Prompt for AI Agents |
||
|
|
||
| def test_problem_update(): | ||
| prob = Problem() | ||
| x1 = prob.addVariable(vtype=INTEGER, lb=0, name="x1") | ||
|
|
||
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.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add type hints and complete API docstrings for new public methods.
setMIPStart/getMIPStartare new public APIs but currently lack type annotations and full docstring sections (Parameters,Returns,Raises).Suggested patch
As per coding guidelines, “Type hints on NEW public functions/classes” and “Docstring CONTENT on new public APIs — params, returns, raises”.
🤖 Prompt for AI Agents