Skip to content

Design JuMP/MOI constrained optimization integration#103

Open
bernalde wants to merge 2 commits into
mainfrom
pr-10-jump-moi-design
Open

Design JuMP/MOI constrained optimization integration#103
bernalde wants to merge 2 commits into
mainfrom
pr-10-jump-moi-design

Conversation

@bernalde

Copy link
Copy Markdown
Member

Summary

  • add an internal architecture document for lowering JuMP/MOI function-in-set constraints into TenSolver's native projection constraint IR
  • define the supported first-pass mapping, projection-cost expectations, fixed-variable and variable-index handling, fallback/error policy, and infeasibility result contract
  • keep TenSolver.Optimizer as the single JuMP entry point and keep the existing native constraint types as the low-level direct Julia API
  • index the design note from the repository documentation guide

This PR is design-only. It does not add functional JuMP constraint support or new dependencies.

Key decisions

  • MOI sets are the user-facing modeling abstraction; AbstractConstraint subtypes remain the compact, projection-aware lowering targets.
  • The constrained path specializes the existing QUBODrivers non-incremental copy flow. It materializes a filtered MOI model, composes index maps, substitutes fixed variables, and only then assigns native solver sites.
  • Compact native patterns are preferred before the general sum lowering, including exactly-one and pairwise exclusion/relation cases.
  • Unsupported constraints fail explicitly. There is no implicit penalty-QUBO or generic feasible-assignment fallback.
  • Point-to-set distance/projection utilities are useful conceptually or for validation, but do not construct TenSolver's Hilbert-space projection MPO.
  • MOI infeasibility must surface as MOI.INFEASIBLE with 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.md
  • julia +release --project=docs/ docs/make.jl local
  • julia +release --project=. -e 'using Pkg; Pkg.test()'
  • git diff --check
  • exercised the proposed filtered-model materialization and composed variable-index mapping against the current MOI/QUBODrivers versions, including fixed-variable elimination

Branch Hygiene

@bernalde
bernalde marked this pull request as ready for review July 16, 2026 23:48

@bernalde bernalde left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 projector P, 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 AbstractSampler getter MOI.get(_, ::MOI.TerminationStatus) (src/library/sampler/wrappers/moi.jl:477) returns MOI.OTHER_ERROR for an empty sample set with nonempty metadata and does not read metadata["termination_status"]. So the doc's instruction to add a concrete TerminationStatus override is necessary, not optional — and it mirrors what QUBODrivers' own MIPSampler driver does (reads metadata["termination_status"] first). Good to have this pinned in the design.
  • Mapping-table rows all check out: sum(x)==n-1ExactlyOne(·,0); x_i+x_j<=1NotEquals([i,j],[1,1]); AllDifferent(2)Relation(!=) with >2 binary statically infeasible; SOS1sum<=1 (bond 3, correctly "at most one", all-zero feasible so not exactly-one); the x_i−x_j relation rows; and the nonnegative-integer canonicalization (sign-flip reverses <=/>=, mixed-sign rejected unless pairwise). RelationConstraint does accept :(!=) (src/constraints.jl).
  • Infeasibility status contract matches the landed #97 behavior and tensolver_status returning MOI.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.

Comment thread docs/internal/jump_moi_constraints.md Outdated
| `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. |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@iagoleal iagoleal Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a new kind of constraint, it would be more interesting if SumConstraint could also accept an interval as its relation.

Comment thread docs/internal/jump_moi_constraints.md Outdated

- MOI `function-in-set` constraints are the high-level modeling interface used
by JuMP.
- TenSolver's native [`AbstractConstraint`](@ref) subtypes remain the small,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bernalde

Copy link
Copy Markdown
Member Author

Addressed both nonblocking review notes in 12e59bc.

Changes:

  • Replaced the two-projector Interval lowering with a future SumIntervalConstraint backed by one capped, two-sided partial-sum DFA. The design now records bond upper + 2, one-sided simplification, and static-infeasibility handling, and the mapping table and implementation stages agree.
  • Replaced Documenter-only @ref syntax in the internal note with plain code spans.

Verification:

  • git diff --check
  • Parsed docs/internal/jump_moi_constraints.md with Julia's Markdown.parse
  • Independently enumerated representative weighted assignments to confirm the capped DFA accepts exactly l <= sum <= u
  • julia +1.12 --project=docs/ docs/make.jl local
  • julia --project -e 'using Pkg; Pkg.test()'
  • All current-head GitHub checks are green, including Documentation, documenter/deploy, and the nine-job Julia matrix.

No review comments were intentionally left unaddressed. The two threads remain unresolved for reviewer confirmation; there is no formal CHANGES_REQUESTED decision. The existing coordination note for PR #46 still applies if its nearby documentation-list edit lands first.

@iagoleal iagoleal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +19
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Agreed.

Comment on lines +81 to +84
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +159 to +167
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having such constraint would be redundant. It makes more sense to adapt SumConstraint to also accept intervals as its relation argument.

Comment on lines +171 to +173
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This table should be reworked taking into account #105 and the soon to come changes from #67.
For this first version of the MOI-interface, I'd rather no assume anything about the domain. Any necessary simplifications can be made on the native TenSolver side.

Comment on lines +199 to +216
## 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is unrelated to what we're going to implement.

Comment on lines +225 to +226
- enumerate every feasible assignment through the legacy generic projection
path; or

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this legacy path has been removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Design JuMP/MOI constrained optimization integration

2 participants