Troubleshooting & pitfalls¶
gr/lm is slow, hangs, or the reduced model won't evaluate¶
Cause: substitution compounds the expression graph. Eliminating a chain of
2-variable-eliminable constraints (y0=f(x), y1=g(y0), …) inlines each into
the next, so the tree size grows toward degree-2^n even though the shared
DAG is small. discopt's expression→jaxpr lowering re-traces shared nodes, so a
few-thousand-node DAG can have a multi-million-node tree — Hessian evaluation
then dominates (the paper's "Hessian evaluation becomes the bottleneck").
Fixes, in order:
1. order="min_fill" — visits low-fan-out candidates first (~40× smaller
expressions on DAE models). Cheapest, always try first.
2. max_def_nodes=<N> — hard cap on substituted expression size (drops the
offending eliminations).
3. Use method="d2" (or d3) instead — structure-preserving methods can't
compound because each definition has ≤2 variables.
4. If the culprit is a cyclic block, use eliminate_implicit (keeps it
implicit, no substitution) — see recovery.md.
Diagnose it:
from discopt.aggregation import aggregate, model_expression_size
a = aggregate(m, method="gr")
print(model_expression_size(a.reduced_model)) # (tree, dag); tree >> dag => blowup
Local vs global solve (important for benchmarking nonconvex models)¶
discopt.Model.solve() runs a global spatial branch-and-bound on nonconvex
NLPs — slow, and NOT the paper's setting (local IPOPT). For runtime/reliability
work use the local solver:
from discopt.solvers.nlp_pounce import solve_nlp_from_model # local NLP
# or the harness helper:
from discopt.aggregation.benchmark import _local_solve # (status, iters, wall, obj)
solve(m, method=...) calls the reduced model's .solve() (global). On a
nonconvex reduced model with a big expression this can appear to "hang" — it's
the global solver, not aggregation. Use _local_solve(aggregate(m, ...).reduced_model)
to time the paper's setting.
solve() returns status="infeasible" unexpectedly¶
This is intentional and usually correct: a fixed-variable elimination produced a value outside that variable's bounds, or a fully-determined system is inconsistent. Check:
a = aggregate(m, method="ld1")
print(a.infeasible_eliminations) # [(var, value, lb, ub), ...] -> the model IS infeasible
Dual recovery reports available=False¶
res.duals.reason tells you why:
- "active_eliminated_bound:<name>" — an eliminated variable is active on a bound
whose box→inequality was pruned (so its γ is unobservable). This cannot
happen for a truly active bound, so it is a defensive decline, not a wrong
answer. A surviving active bound is now recovered (DU2) via
res.duals.bound_duals — see recovery.md.
- "missing_reduced_duals" — the backend didn't expose constraint duals.
- "unsupported_derivative_or_singular" / "residual_check_failed" — a
derivative couldn't be formed, or the KKT self-check failed; recovery declines
rather than return a wrong multiplier.
Single-pass and recursive (multi-pass) paths are both supported for
minimize and maximize objectives, and for active or interior
eliminated-variable bounds (their γ-terms land in res.duals.bound_duals). A
maximize objective is reported by discopt in the minimize(-f) convention;
recovery normalises the sign, so recovered duals stay directly comparable to a
direct solve (no "maximise" bail).
Nothing gets eliminated¶
- The model may have no variable defined by an equality (aggregation needs
y - f(...) == 0forms, not just any constraint). - Array variables: ensure
scalarize=True(default). If an array is used whole (sum(a), a slice, a matmul) it abstains — index it elementwise to make it eliminable. pivot_toltoo high for a poorly-scaled model — trypivot_tol=0.0.- The defining variable appears nonlinearly in its own equation (e.g.
y + y**3 == x) — it can't be isolated, so it's not a candidate.
Results differ between runs¶
They shouldn't — the pipeline is deterministic across PYTHONHASHSEED. If you
see variation, you're likely comparing across a code change or a different
method/order. Confirm with:
for s in 0 5 7; do PYTHONHASHSEED=$s python -c "from discopt.aggregation import aggregate; from discopt.aggregation.testproblems import reaction_diffusion; print(sorted(aggregate(reaction_diffusion(8), method='lm').eliminated))"; done
MINLP: binaries/integers¶
Not eliminated by default — aggregation targets scalar continuous variables
unless you opt in. Pass integer_aggregation=True to also eliminate a scalar
integer/binary variable when its integrality is implied (the conditions below);
otherwise integer/binary variables are left untouched.
An integer variable is not eliminated¶
By default integers are never candidates — pass integer_aggregation=True. Even
then, elimination is admitted only when integrality is implied; if your integer
is not eliminated with the flag on, check the four conditions: the defining
equality must be fully affine (no nonlinear term), the pivot ±1 (so
y = 2*i cannot eliminate i), every other participant integer/binary (a
continuous variable in the definition abstains), and every other coefficient
and the constant integral (fractional data abstains). Integers appearing only
in inequalities (e.g. on/off binaries x <= cap*b) are never eliminable — they
are not defined by an equality.