Recently I have been working on better debugging of CP-SAT models. In order to test the various functions, I wanted to load a saved model and find variables and constraints. It turns out, I haven’t ever loaded a model from a pbtxt dump file. I searched in the forums and GitHub issues, and the best advice I found didn’t work. This short blog (Hello 2026!) is about how to load a CP-SAT model from a protobuf text dump in Python, as of version 9.16 of OR-Tools.
The existing advice doesn’t work
As always, I’m showing my age by saying that the first thing I do when I hit a programming snag is that I search the mailing list and issues. (I leave the use of plagiarism bots to the kids.) There was a recent question from 2024 on this topic and the best answer gave a short snippet of code as follows:
from ortools.sat.python import cp_model
from google.protobuf import text_format
model = cp_model.CpModel()
with open("model.txt", "r") as file:
text_format.Parse(file.read(), model.Proto())
So I tried that, and it completely fell flat:
$ python simple_load_file.py
Traceback (most recent call last):
File "/home/james/repos/activimetrics/projects/2026/paa/paa_project/paa_solver/call_scheduler/simple_load_file.py", line 6, in <module>
text_format.Parse(file.read(), model.Proto())
File "/home/james/repos/activimetrics/projects/2026/paa/paa_project/.venv/lib64/python3.12/site-packages/google/protobuf/text_format.py", line 728, in Parse
return ParseLines(text.split(b'\n' if isinstance(text, bytes) else u'\n'),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/james/repos/activimetrics/projects/2026/paa/paa_project/.venv/lib64/python3.12/site-packages/google/protobuf/text_format.py", line 805, in ParseLines
return parser.ParseLines(lines, message)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/james/repos/activimetrics/projects/2026/paa/paa_project/.venv/lib64/python3.12/site-packages/google/protobuf/text_format.py", line 858, in ParseLines
self._ParseOrMerge(lines, message)
File "/home/james/repos/activimetrics/projects/2026/paa/paa_project/.venv/lib64/python3.12/site-packages/google/protobuf/text_format.py", line 886, in _ParseOrMerge
self.root_type = message.DESCRIPTOR.full_name
^^^^^^^^^^^^^^^^^^
AttributeError: 'ortools.sat.python.cp_model_helper.CpModelProto' object has no attribute 'DESCRIPTOR'
Ugh. No amount of searching was giving me any joy on what could be the issue, and my coffee was getting cold.
A different way
I already had some bits of code in which I read in a partial constraint dump and got back an actual constraint object.
from ortools.sat.python import (cp_model_helper as cmh, cp_model)
...
# I actually read this using a regex from the solver output
matched_failing_constraint = "bool_or { literals: 841 literals: 861 literals: 879 literals: 901 }"
echoed_constraint = cmh.ConstraintProto()
parse_okay = echoed_constraint.parse_text_format(matched_failing_constraint)
print(echoed_constraint)
print(matched_failing_constraint)
# visually, this confirms that the parsed text is a real constraint
I sort of stumbled on that by accident, after skimming the protobuf docs and
seeing promising looking method names in my emacs python lsp mode. In hindsight,
I could have gotten there from the CP-SAT source code itself. Looking at
wrappers.cc, ParseFromString is renamed in python:
// Generates a pybind11 wrapper class declaration for a top level message.
void GenerateTopLevelMessageDecl(const google::protobuf::Descriptor& msg) {
...
.def("parse_text_format",
[](std::shared_ptr<$0> self, const std::string& text) {
return google::protobuf::TextFormat::ParseFromString(text, self.get());
})
Plus there is the bit of code using this in the file cp_model_helper_test.py:
class CpModelHelperTest(absltest.TestCase):
def test_simple_solve(self):
model_string = """
variables { domain: -10 domain: 10 }
variables { domain: -10 domain: 10 }
variables { domain: -461168601842738790 domain: 461168601842738790 }
constraints {
linear {
vars: 0
... blah blah blah ...
objective {
vars: 2
coeffs: -1
scaling_factor: -1
}"""
model = cmh.CpModelProto()
self.assertTrue(model.parse_text_format(model_string))
...
The way I understand it, the protobuf objects are designed around the message, and they can read and write their own messages. It seemed like the sample code was doing just that, but clearly something was off.
Anyway, my successful read in a pbtxt program is as follows:
from ortools.sat.python import cp_model_helper as cmh, cp_model
__author__ = "James E. Marca"
__copyright__ = "James E. Marca"
__license__ = "Apache License, Version 2.0"
def parse_model_file():
"""Parse a model file."""
test_file = "tests/data/dumpmodel_20260401_20260402_CpSolverStatus.INFEASIBLE.pbtxt"
model = cp_model.CpModel()
model_proto: cmh.CpModelProto = model.Proto()
with open(test_file, "r") as file:
model_proto.parse_text_format(str(file.read()))
model_constraints = model_proto.constraints
model_variables = model_proto.variables
print(f"I have {len(model_constraints)} constraints")
assert len(model_constraints) > 1
print(f"I have {len(model_variables)} variables")
assert len(model_variables) > 1
parse_model_file()
Running it, I see this:
$ python test_read.py
I have 39549 constraints
I have 9829 variables