Design JuMP/MOI constrained optimization integration#103
Conversation
bernalde
left a comment
There was a problem hiding this comment.
Maintainer review — design-only PR for #65
Docs-only change: adds docs/internal/jump_moi_constraints.md (+358) and three lines to docs/DOCUMENTATION.md. No code, no dependencies. I authored this PR, so it posts as COMMENT (GitHub rejects author APPROVE); main is not branch-protected and GitHub reports no review gate, so no formal approval is enforced — but a second maintainer's read is worthwhile for a design that will steer an implementation epic.
Acceptance criteria (#65) — all met
- Specific enough for a follow-up epic: yes — a staged 5-part implementation plan plus a design acceptance checklist.
- Separates native Julia API from MOI bridge: yes — "Current Boundary" and "Public API Decision".
- Native types as lowering targets for MOI sets/bridges, incl. point-on-set: yes — "Point-on-Set Versus Projection MPOs" correctly distinguishes
distance_to_set/MathOptSetDistances (numeric point projection) from TenSolver's Hilbert-space diagonal projectorP, and declines the extra dependency. - Lists unsupported constraints + fallback: yes — explicit hard-error policy, no implicit penalty-QUBO or generic-enumeration fallback.
- References CoTenN: yes.
Verified technical claims
Checked the load-bearing claims against the resolved dependency versions rather than taking them on faith:
- QUBODrivers status override — correct and load-bearing. Against QUBODrivers v0.6.5, the generic
AbstractSamplergetterMOI.get(_, ::MOI.TerminationStatus)(src/library/sampler/wrappers/moi.jl:477) returnsMOI.OTHER_ERRORfor an empty sample set with nonempty metadata and does not readmetadata["termination_status"]. So the doc's instruction to add a concreteTerminationStatusoverride is necessary, not optional — and it mirrors what QUBODrivers' ownMIPSamplerdriver does (readsmetadata["termination_status"]first). Good to have this pinned in the design. - Mapping-table rows all check out:
sum(x)==n-1→ExactlyOne(·,0);x_i+x_j<=1→NotEquals([i,j],[1,1]);AllDifferent(2)→Relation(!=)with >2 binary statically infeasible;SOS1→sum<=1(bond 3, correctly "at most one", all-zero feasible so not exactly-one); thex_i−x_jrelation rows; and the nonnegative-integer canonicalization (sign-flip reverses<=/>=, mixed-sign rejected unless pairwise).RelationConstraintdoes accept:(!=)(src/constraints.jl). - Infeasibility status contract matches the landed #97 behavior and
tensolver_statusreturningMOI.INFEASIBLE.
Findings
Two Nonblocking notes inline; no blocking issues.
Tests / CI
Docs-only. CI on ec343ab is green across all 10 checks (build, Documentation, and the 9-way Julia matrix). The note lives under docs/internal/ and is not in docs/make.jl's page list, so Documenter does not process it — consistent with the existing spinglasspeps_integration.md.
Merge-readiness
No blocking findings; the two inline notes are optional polish for the follow-up epic. One coordination item (already called out in the PR description): PR #46 also appends to the internal-notes list in docs/DOCUMENTATION.md; the edits are disjoint, so whichever lands second needs a one-line resync. This account cannot post a formal APPROVE on its own PR, and no approval gate is reported as required.
| | `sum(x) == length(x) - 1` | `ExactlyOneConstraint(sites, 0)` | 2 | Exactly one selected site is zero. | | ||
| | `x[i] + x[j] <= 1` | `NotEqualsConstraint([i, j], [1, 1])` | 2 | Compact pairwise exclusion. | | ||
| | Nonnegative integer affine function in `EqualTo`, `LessThan`, or `GreaterThan` | `SumConstraint` with `==`, `<=`, or `>=` | `rhs + 2` | Constants are moved into `rhs`; reject invalid normalized data. | | ||
| | Nonnegative integer affine function in `Interval` | Two `SumConstraint`s | Product of the two projection costs | Lower the lower and upper bounds separately; drop a redundant infinite side. | |
There was a problem hiding this comment.
Nonblocking: lowering an interval to two SumConstraints gives a bond of roughly (l+2)·(u+2) (the product of the two one-sided projectors), but a single two-sided partial-sum automaton — states 0..u+1, accepting l..u — enforces l <= sum <= u with bond u+2, strictly smaller. That is exactly the "useful projection target with a specialized DFA" the Public API Decision section says warrants a native type, so consider framing an interval/two-sided SumConstraint as the more compact future lowering rather than presenting the product as the expected cost.
There was a problem hiding this comment.
Addressed in 12e59bc: Interval now targets a future SumIntervalConstraint using one capped two-sided partial-sum DFA, with bond upper + 2; redundant one-sided bounds still simplify to SumConstraint. The mapping table and follow-up implementation stage were updated, and representative weighted cases were independently enumerated against the interval predicate.
There was a problem hiding this comment.
Instead of adding a new kind of constraint, it would be more interesting if SumConstraint could also accept an interval as its relation.
|
|
||
| - MOI `function-in-set` constraints are the high-level modeling interface used | ||
| by JuMP. | ||
| - TenSolver's native [`AbstractConstraint`](@ref) subtypes remain the small, |
There was a problem hiding this comment.
Nonblocking: this file lives in docs/internal/ and is not in docs/make.jl's page list, so Documenter never resolves these @ref links — on GitHub, where an internal design note is actually read, they render as text linking to a literal @ref anchor. The sibling spinglasspeps_integration.md does the same, so this is consistent; but since neither is Documenter-built, plain code spans (or full docs URLs for public API names) would read cleaner. Purely cosmetic.
There was a problem hiding this comment.
Addressed in 12e59bc: all Documenter-only @ref links in this internal note are now plain code spans. I verified that no @ref syntax remains and that Julia's Markdown parser accepts the note.
|
Addressed both nonblocking review notes in Changes:
Verification:
No review comments were intentionally left unaddressed. The two threads remain unresolved for reviewer confirmation; there is no formal |
iagoleal
left a comment
There was a problem hiding this comment.
Its mostly good to go. I've left some comments.
The most important point to keep in mind:
- This design document should coordinate/take into account the changes in #44 (multiple backends), #105 (non-binary data) and #68 (polynomial objectives via JuMP). Even if some are not implemented here, the interface should be extensible enough to account for those changes. In particular, #105 already landed.
| The central decision is to keep two layers with different responsibilities: | ||
|
|
||
| - MOI `function-in-set` constraints are the high-level modeling interface used | ||
| by JuMP. | ||
| - TenSolver's native `AbstractConstraint` subtypes remain the small, | ||
| projection-aware intermediate representation (IR) consumed by the solver. | ||
|
|
||
| The MOI layer must lower into the native IR. It must not teach the projection | ||
| code to dispatch on arbitrary MOI sets, and it must not turn supported hard | ||
| constraints into penalty terms. |
| 5. The materialized model is passed to the QUBODrivers `AbstractSampler` copy | ||
| path, which keeps ownership of QUBO construction and fixed-variable | ||
| objective substitution. TenSolver composes this second index map with the | ||
| first. |
There was a problem hiding this comment.
I'm not sure if it makes sense to keep using QUBODrivers now that we support constraint. An option would be to move this effort to QUBODrivers and open a PR adding the necessary support there.
| A genuine two-sided bound `l <= sum(a[i] * x[i]) <= u` should likewise use one | ||
| specialized projection rather than compose separate lower- and upper-bound | ||
| projectors. A future `SumIntervalConstraint` can use partial-sum states | ||
| `0:u+1`, send every sum above `u` to the overflow state `u+1`, and accept | ||
| states `l:u`. Its projection MPO therefore has bond at most `u + 2`, instead of | ||
| the product of the two one-sided bonds. This compact DFA justifies a native | ||
| type under the public-API rule above. Normalization should still reduce an | ||
| interval with a redundant side to the existing one-sided `SumConstraint`, or | ||
| report static infeasibility when its bounds cannot contain a reachable sum. |
There was a problem hiding this comment.
Having such constraint would be redundant. It makes more sense to adapt SumConstraint to also accept intervals as its relation argument.
| All rows below require `ZeroOne` variables. “Bond” is the expected projection | ||
| MPO bond bound after lowering, based on the current native DFA methods or, for | ||
| the future interval target, the specialized DFA specified above. |
There was a problem hiding this comment.
With #105, we can no longer assume the variables are binary. Removing this assumption also puts our codebase on a par with CoTenN.
| | `x[i] - x[j]` in `EqualTo(0)`, `LessThan(0)`, or `GreaterThan(0)` | `RelationConstraint(i, relation, j)` | 2 | Covers equality, implication-style `<=`, and `>=`. | | ||
| | `VectorOfVariables([x[i], x[j]])` in `MOI.AllDifferent(2)` | `RelationConstraint(i, :(!=), j)` | 2 | `AllDifferent` over more than two binary variables is statically infeasible. | | ||
| | `VectorOfVariables(x)` in `MOI.SOS1(weights)` | `SumConstraint(sites, ones, 1; relation = :(<=))` | 3 | `SOS1` means *at most* one nonzero, not exactly one; ordering weights do not change feasibility. | | ||
| | `ScalarAffineFunction` in a TenSolver `NotEqualTo(value)` set | `SumConstraint(...; relation = :(!=))`, or `RelationConstraint` for a pairwise pattern | Native target's bound | MOI has no standard scalar not-equal set. | |
| ## Point-on-Set Versus Projection MPOs | ||
|
|
||
| MOI's function-in-set vocabulary is the right abstraction for declaring the | ||
| feasible set. It should not be confused with the operator that TenSolver builds. | ||
|
|
||
| `MOI.Utilities.distance_to_set` and the separate | ||
| [MathOptSetDistances.jl](https://github.com/matbesancon/MathOptSetDistances.jl) | ||
| package operate on numeric points and MOI sets. They can support validation or | ||
| diagnostics, but they do not construct TenSolver's projection MPO. TenSolver's | ||
| `P` is a Hilbert-space operator that is diagonal in the binary computational | ||
| basis, with value one on feasible bitstrings and zero on infeasible bitstrings. | ||
|
|
||
| Therefore the first bridge needs no new set-distance dependency. The useful | ||
| connection is conceptual: the MOI set defines membership, the native constraint | ||
| defines a compact automaton for that membership predicate, and the automaton | ||
| defines `P`. | ||
|
|
||
| This separation also leaves room for future non-diagonal projections, such as |
There was a problem hiding this comment.
This section is unrelated to what we're going to implement.
| - enumerate every feasible assignment through the legacy generic projection | ||
| path; or |
There was a problem hiding this comment.
this legacy path has been removed.
Summary
TenSolver.Optimizeras the single JuMP entry point and keep the existing native constraint types as the low-level direct Julia APIThis PR is design-only. It does not add functional JuMP constraint support or new dependencies.
Key decisions
AbstractConstraintsubtypes remain the compact, projection-aware lowering targets.MOI.INFEASIBLEwith zero results; the implementation must override the generic empty-sample QUBODrivers status behavior.Coordination
Validation
julia +release --startup-file=no -e 'using Markdown; for file in ARGS; Markdown.parse(read(file, String)); println("parsed ", file); end' docs/internal/jump_moi_constraints.md docs/DOCUMENTATION.mdjulia +release --project=docs/ docs/make.jl localjulia +release --project=. -e 'using Pkg; Pkg.test()'git diff --checkBranch Hygiene
mainat009e2404ba34f39b9b23bb199d0e85a20d83a9db.pr-10-jump-moi-design.docs/DOCUMENTATION.mdin the adjacent public-page inventory. The changes are semantically disjoint, but this branch should be resynced if Add PEPS docs, examples, and benchmarks #46 lands first.