Skip to content

Commit 1fe5782

Browse files
committed
fix: updating for ortools 9.15 which changed some behavior. Also more reliable replacement for checking hints.
1 parent b062283 commit 1fe5782

6 files changed

Lines changed: 98 additions & 33 deletions

File tree

chapters/04_modelling.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -826,10 +826,12 @@ model.add_abs_equality(target=abs_xz, expr=x + z)
826826

827827
# Create variables to capture the maximum and minimum of x, (y-1), and z
828828
max_xyz = model.new_int_var(0, 100, "max(x, y-1, z)")
829-
model.add_max_equality(target=max_xyz, exprs=[x, y - 1, z])
829+
# Changed in ortools 9.15: was add_max_equality(target=max_xyz, exprs=[x, y - 1, z])
830+
model.add_max_equality(max_xyz, x, y - 1, z)
830831

831832
min_xyz = model.new_int_var(-100, 100, "min(x, y-1, z)")
832-
model.add_min_equality(target=min_xyz, exprs=[x, y - 1, z])
833+
# Changed in ortools 9.15: was add_min_equality(target=min_xyz, exprs=[x, y - 1, z])
834+
model.add_min_equality(min_xyz, x, y - 1, z)
833835
```
834836

835837
While some practitioners report that these methods are more efficient than those

chapters/05_parameters.md

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ load the model on a different machine and run the solver.
498498

499499
```python
500500
from ortools.sat.python import cp_model
501+
from ortools.sat import cp_model_pb2
501502
from google.protobuf import text_format
502503
from pathlib import Path
503504

@@ -508,20 +509,39 @@ def _detect_binary_mode(filename: str) -> bool:
508509
return True
509510
raise ValueError(f"Unknown extension for file: {filename}")
510511

512+
# Changed in ortools 9.15: was model.Proto().SerializeToString() / text_format.MessageToString()
513+
# Now use model.export_to_file() which auto-detects format by extension
511514
def export_model(model: cp_model.CpModel, filename: str, binary: bool | None = None):
512515
binary = _detect_binary_mode(filename) if binary is None else binary
513-
if binary:
514-
Path(filename).write_bytes(model.Proto().SerializeToString())
516+
# export_to_file uses .txt extension for text format, otherwise binary
517+
# So we need to handle the mismatch for some extensions
518+
if binary and filename.endswith(".txt"):
519+
# Force binary even with .txt extension - use temp file
520+
temp_file = filename + ".pb"
521+
model.export_to_file(temp_file)
522+
Path(filename).write_bytes(Path(temp_file).read_bytes())
523+
Path(temp_file).unlink()
524+
elif not binary and not filename.endswith(".txt"):
525+
# Force text even without .txt extension
526+
temp_file = filename + ".txt"
527+
model.export_to_file(temp_file)
528+
Path(filename).write_text(Path(temp_file).read_text())
529+
Path(temp_file).unlink()
515530
else:
516-
Path(filename).write_text(text_format.MessageToString(model.Proto()))
531+
model.export_to_file(filename)
517532

533+
# Changed in ortools 9.15: was model.Proto().ParseFromString() / text_format.Parse()
534+
# Now use model.Proto().parse_text_format() for text, or cp_model_pb2 for binary
518535
def import_model(filename: str, binary: bool | None = None) -> cp_model.CpModel:
519536
binary = _detect_binary_mode(filename) if binary is None else binary
520537
model = cp_model.CpModel()
521538
if binary:
522-
model.Proto().ParseFromString(Path(filename).read_bytes())
539+
# Parse binary via standard protobuf, then convert to text for import
540+
proto = cp_model_pb2.CpModelProto()
541+
proto.ParseFromString(Path(filename).read_bytes())
542+
model.Proto().parse_text_format(text_format.MessageToString(proto))
523543
else:
524-
text_format.Parse(Path(filename).read_text(), model.Proto())
544+
model.Proto().parse_text_format(Path(filename).read_text())
525545
return model
526546
```
527547

@@ -560,17 +580,26 @@ We will also see how to utilize hints for multi-objective optimization in the
560580
> complete the solution from the hint, it may have wasted a lot of time in
561581
> branches it could otherwise have pruned.
562582
563-
To ensure your hints are correct, you can enable the following parameter, which
564-
will make CP-SAT throw an error if the hints are incorrect:
583+
To verify that your hints are feasible, you can temporarily fix variables to
584+
their hinted values and check if the model becomes infeasible:
565585

566586
```python
567-
solver.parameters.debug_crash_on_bad_hint = True
587+
solver.parameters.fix_variables_to_their_hinted_value = True
588+
status = solver.solve(model)
589+
if status == cp_model.INFEASIBLE:
590+
print("Hints are conflicting or infeasible!")
568591
```
569592

570593
If you suspect that your hints are not being utilized, it might indicate a
571-
logical error in your model or a bug in your code. This parameter can help
572-
diagnose such issues. However, this feature does not work reliably, so it should
573-
not be solely relied upon.
594+
logical error in your model or a bug in your code. This approach reliably
595+
detects infeasible hints by forcing the solver to use the exact hinted values.
596+
597+
There is also `solver.parameters.debug_crash_on_bad_hint = True`, which crashes
598+
the solver if it cannot complete hints into a feasible solution. However, this
599+
feature is unreliable: it only triggers in multi-worker mode, depends on a race
600+
condition between workers, and is controlled by `hint_conflict_limit` (default:
601+
10). The `fix_variables_to_their_hinted_value` approach above is simpler and
602+
deterministic.
574603

575604
> [!WARNING]
576605
>

tests/test_abs_minmax.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ def test_lin_constraints():
1212
model.add_abs_equality(target=abs_xz, expr=x + z)
1313
# max_xyz = max(x,y,z-1)
1414
max_xyz = model.new_int_var(0, 100, "max(x,y, z-1)")
15-
model.add_max_equality(target=max_xyz, exprs=[x, y, z - 1])
15+
# Changed in ortools 9.15: was add_max_equality(target=max_xyz, exprs=[x, y, z - 1])
16+
model.add_max_equality(max_xyz, x, y, z - 1)
1617
# min_xyz = min(x,y,z)
1718
min_xyz = model.new_int_var(-100, 100, " min(x,y, z)")
18-
model.add_min_equality(target=min_xyz, exprs=[x, y, z])
19+
# Changed in ortools 9.15: was add_min_equality(target=min_xyz, exprs=[x, y, z])
20+
model.add_min_equality(min_xyz, x, y, z)

tests/test_interval.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ def test_interval():
3838
name="optional_fixed_interval",
3939
)
4040

41+
# Changed in ortools 9.15: was interval_vars=
4142
model.add_no_overlap(
42-
interval_vars=[
43+
intervals=[
4344
flexible_interval,
4445
fixed_interval,
4546
optional_interval,

tests/test_parameters.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,21 +127,24 @@ def test_assumptions():
127127

128128

129129
def test_bad_hints():
130+
"""
131+
Test that fix_variables_to_their_hinted_value detects infeasible hints.
132+
This is more reliable than debug_crash_on_bad_hint which has race conditions.
133+
"""
130134
model = cp_model.CpModel()
131-
vertices = range(30)
135+
vertices = range(10)
132136
arcs = [(i, j) for i in vertices for j in vertices if i != j]
133-
costs = {(i, j): random.randint(1, 100) for i, j in arcs}
134137
x = {(i, j): model.new_bool_var(f"x_{i}_{j}") for i, j in arcs}
135138
model.add_circuit([(v, w, x) for (v, w), x in x.items()])
136-
model.minimize(sum(costs[i, j] * x[i, j] for i, j in arcs))
139+
# add a bad hint of multiple outgoing arcs from the same node (violates circuit)
137140
model.add_hint(x[0, 1], 1)
138141
model.add_hint(x[0, 2], 1)
139142
model.add_hint(x[0, 3], 1)
140143
solver = cp_model.CpSolver()
141-
# add a bad hint of two outgoing arcs from the same node
142-
solver.parameters.max_time_in_seconds = 10
143-
solver.parameters.debug_crash_on_bad_hint = True
144-
status = solver.solve(model) # noqa: F841
144+
# Fix variables to hinted values to reliably detect infeasible hints
145+
solver.parameters.fix_variables_to_their_hinted_value = True
146+
status = solver.solve(model)
147+
assert status == cp_model.INFEASIBLE, "Expected INFEASIBLE due to conflicting hints"
145148

146149

147150
def test_presolve_parameters_exist():

tests/test_serialization.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
def test_serialization():
55
from ortools.sat.python import cp_model
6+
from ortools.sat import cp_model_pb2
67
from google.protobuf import text_format
78
from pathlib import Path
89

@@ -13,28 +14,55 @@ def _detect_binary_mode(filename: str) -> bool:
1314
return True
1415
raise ValueError(f"Unknown extension for file: {filename}")
1516

17+
# Changed in ortools 9.15: was model.Proto().SerializeToString() / text_format.MessageToString()
18+
# Now use model.export_to_file() which auto-detects format by extension
1619
def export_model(
1720
model: cp_model.CpModel, filename: str, binary: bool | None = None
1821
):
1922
binary = _detect_binary_mode(filename) if binary is None else binary
20-
if binary:
21-
Path(filename).write_bytes(model.Proto().SerializeToString())
23+
# export_to_file uses .txt extension for text format, otherwise binary
24+
# So we need to handle the mismatch for some extensions
25+
if binary and filename.endswith(".txt"):
26+
# Force binary even with .txt extension - use temp file
27+
temp_file = filename + ".pb"
28+
model.export_to_file(temp_file)
29+
Path(filename).write_bytes(Path(temp_file).read_bytes())
30+
Path(temp_file).unlink()
31+
elif not binary and not filename.endswith(".txt"):
32+
# Force text even without .txt extension
33+
temp_file = filename + ".txt"
34+
model.export_to_file(temp_file)
35+
Path(filename).write_text(Path(temp_file).read_text())
36+
Path(temp_file).unlink()
2237
else:
23-
Path(filename).write_text(text_format.MessageToString(model.Proto()))
38+
model.export_to_file(filename)
2439

40+
# Changed in ortools 9.15: was model.Proto().ParseFromString() / text_format.Parse()
41+
# Now use model.Proto().parse_text_format() for text, or cp_model_pb2 for binary
2542
def import_model(filename: str, binary: bool | None = None) -> cp_model.CpModel:
2643
binary = _detect_binary_mode(filename) if binary is None else binary
2744
model = cp_model.CpModel()
2845
if binary:
29-
model.Proto().ParseFromString(Path(filename).read_bytes())
46+
# Parse binary via standard protobuf, then convert to text for import
47+
proto = cp_model_pb2.CpModelProto()
48+
proto.ParseFromString(Path(filename).read_bytes())
49+
model.Proto().parse_text_format(text_format.MessageToString(proto))
3050
else:
31-
text_format.Parse(Path(filename).read_text(), model.Proto())
51+
model.Proto().parse_text_format(Path(filename).read_text())
3252
return model
3353

3454
def rename_variable_names(model: cp_model.CpModel):
35-
for i, var in enumerate(model.proto.variables):
55+
for i, var in enumerate(model.Proto().variables):
3656
var.name = f"x{i}"
3757

58+
# Changed in ortools 9.15: Proto() objects no longer support == comparison
59+
# Compare via text representation instead
60+
def protos_equal(m1: cp_model.CpModel, m2: cp_model.CpModel) -> bool:
61+
with tempfile.TemporaryDirectory() as td:
62+
m1.export_to_file(f"{td}/m1.txt")
63+
m2.export_to_file(f"{td}/m2.txt")
64+
return Path(f"{td}/m1.txt").read_text() == Path(f"{td}/m2.txt").read_text()
65+
3866
model = cp_model.CpModel()
3967
x = [model.NewIntVar(0, 10, f"x{i}") for i in range(10)]
4068
model.add(sum(x) <= 20)
@@ -43,16 +71,16 @@ def rename_variable_names(model: cp_model.CpModel):
4371
with tempfile.TemporaryDirectory() as tmpdir:
4472
export_model(model, f"{tmpdir}/model.pb")
4573
model2 = import_model(f"{tmpdir}/model.pb")
46-
assert model.Proto() == model2.Proto()
74+
assert protos_equal(model, model2)
4775
export_model(model, f"{tmpdir}/model.txt", binary=False)
4876
model3 = import_model(f"{tmpdir}/model.txt", binary=False)
49-
assert model.Proto() == model3.Proto()
77+
assert protos_equal(model, model3)
5078
export_model(model, f"{tmpdir}/model.pbtxt", binary=False)
5179
model4 = import_model(f"{tmpdir}/model.pbtxt", binary=False)
52-
assert model.Proto() == model4.Proto()
80+
assert protos_equal(model, model4)
5381
export_model(model, f"{tmpdir}/model.pb.txt", binary=True)
5482
model5 = import_model(f"{tmpdir}/model.pb.txt", binary=True)
55-
assert model.Proto() == model5.Proto()
83+
assert protos_equal(model, model5)
5684
rename_variable_names(model)
5785
export_model(model, f"{tmpdir}/model_renamed.pb.txt")
5886
model6 = import_model(f"{tmpdir}/model_renamed.pb.txt")

0 commit comments

Comments
 (0)