Skip to content

[ENH] set_cover Python: expose the lower bound accessors directly - #5277

Open
jg-codes wants to merge 1 commit into
google:mainfrom
jg-codes:expose-set-cover-lower-bound
Open

[ENH] set_cover Python: expose the lower bound accessors directly#5277
jg-codes wants to merge 1 commit into
google:mainfrom
jg-codes:expose-set-cover-lower-bound

Conversation

@jg-codes

@jg-codes jg-codes commented Aug 1, 2026

Copy link
Copy Markdown

Edited 2026-09-05: shortened, the cross-reference to #5122 dropped so this PR stands on its own, and the limits of the existing workaround spelled out. Code unchanged. Re-built and re-run against main @ 5a1b660 on 2026-09-05, and provided cost estimate of the existing workaround.

What

SetCoverInvariant has LowerBound(), CostOrLowerBound() and is_cost_consistent() in C++. None of the three are bound in ortools/set_cover/python/set_cover.cc. This adds them next to the existing cost binding (plus two tests).

DualAscentOptimizer writes its dual bound onto the invariant through
ReportLowerBound(). From Python, the only way to read it today is:

bound = inv.export_solution_as_proto().cost_lower_bound

That path has two problems.
i) It walks every subset in the model and copies the selected ones into a proto in order to hand back one double, so costing a full pass over the model every time by reading a gap inside. Measured on main @ 5a1b660 with this PR applied, one read costs about 28 ns per subset: 0.8 ms at 3,000 subsets (scpb1), 28 ms at 1,000,000 subsets, against 0.9 µs for inv.lower_bound(). And ExportSolutionAsProto() writes the raw lower_bound_, while SetCoverSolutionResponse has no field for this consistency flag.

A Python caller therefore cannot tell whether that number is a live bound or a stale
one, and CostOrLowerBound(), which branches on that flag, cannot be
reconstructed from Python.

With the bindings:

gap = (inv.cost() - inv.lower_bound()) / inv.cost()

is_cost_consistent() is included for the same reason: ReportLowerBound() is called with is_cost_consistent=false. After a DualAscentOptimizer run on scpb1, for example, cost() is 75, lower_bound() is 45, is_cost_consistent() is False and cost_or_lower_bound() is 45; the proto shows only the 75 and the 45.

Three .def() lines and two tests. No new dependency, no build change.

Verification

bazel test //ortools/set_cover/python:set_cover_test passes on Linux (Bazel 8.7.0,
x86_64) against main @ 5a1b660 (2026-09-05). clang-format clean against the repo .clang-format; black clean at the file's existing line width.

CLA signed. Drafted with Claude; built, run and reviewed by me.

DualAscentOptimizer computes a dual lower bound and stores it via
inv()->ReportLowerBound() (set_cover_heuristics.cc:1276, :1314).
SetCoverInvariant exposes it in C++ as LowerBound() and
CostOrLowerBound(), neither of which is bound in the pybind11 wrapper.

The value is reachable from Python today, just indirectly:
ExportSolutionAsProto() serializes it into
SetCoverSolutionResponse.cost_lower_bound (set_cover_invariant.cc:536),
and that binding exists. But reading a scalar today means serializing
the whole solution proto, which is awkward when the caller wants an
optimality gap next to inv.cost().

Binds LowerBound(), CostOrLowerBound() and is_cost_consistent(). The
last one is needed because ReportLowerBound() is called with
is_cost_consistent=false, so a caller has to be able to tell whether
the invariant currently holds a feasible solution or a relaxation,
which is exactly what CostOrLowerBound() switches on.

Adds two tests.

Built and tested on Linux (Bazel 8.7.0) against main at d9c0910:
bazel test //ortools/set_cover/python:set_cover_test passes.
@jg-codes

jg-codes commented Aug 1, 2026

Copy link
Copy Markdown
Author

For context: this depends on nothing else open right now, but is part of the same set_cover wrapper audit as #5121, #5122, and #5278.

@Mizux Mizux added the Solver: Set Cover Solver in set_cover/ label Aug 2, 2026
@Mizux Mizux modified the milestones: v10.0, v10.0 Beta Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Solver: Set Cover Solver in set_cover/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants