fix(differentiable): support vector / mixed-shape constraints (closes #324)#326
Merged
Merged
Conversation
…324) differentiable_solve's envelope-theorem sensitivity assembled per-constraint values with jnp.array([cf(...) for cf in constraint_fns]) — a *stack*, valid only when every constraint is scalar. The multipliers vector from the NLP solve is per constraint *row* (the evaluator solves over concat_constraints, which ravels each constraint with reshape(-1)), so the stack mis-matches: mixed shapes fail to concatenate (the reported TypeError), and even equal shapes mis-align the lambda·g dot (e.g. mults length 6 vs con_vals shape (2,3)). So the path was broken for ANY non-scalar constraint, not just mixed shapes. Fix: ravel + concatenate each constraint value jnp.concatenate([jnp.ravel(cf(...)) for cf in constraint_fns]) which matches the per-row multiplier ordering (verified: differentiable.py and NLPEvaluator both build constraint_fns via [c for c in model._constraints if isinstance(c, Constraint)] — same filter/order — and the evaluator ravels with reshape(-1)). For scalar constraints this is identical to the old behavior. The same stacking pattern existed at FIVE sites, all fixed: differentiable_solve, _compute_sensitivity_at_solution, solve_fn_jvp (the custom-JVP path), implicit_differentiate, and differentiable_solve_l3 — the issue only named the L1 path, but the L3/implicit path crashed identically (a regression test caught it). Regression: test_differentiable_vector_constraints.py — mixed-shape and equal-shape vector constraints no longer crash on the L1 and L3 paths, and the gradient is checked CORRECT against a central finite difference on a binding model (analytic d(obj)/dp = -2.1, exact match) — guarding both the crash and a silent multiplier-ordering regression. Scalar control unregressed; full differentiable suite (78) passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #324.
differentiable_solve's envelope-theorem sensitivity step assembled per-constraint values withjnp.array([cf(...) for cf in constraint_fns])— a stack, valid only when every constraint is scalar. The multipliers vector from the NLP solve is per constraint row (the evaluator solves overconcat_constraints, which ravels each constraint withreshape(-1)), so the stack mismatches.Broader than the issue title: the path was broken for any non-scalar constraint, not just mixed shapes — mixed shapes fail to concatenate (the reported
TypeError), and even equal shapes mis-align theλ·gdot (multslength 6 vscon_valsshape(2,3)).Fix
Ravel + concatenate each constraint value, matching the per-row multiplier ordering:
Verified the ordering is correct (not just non-crashing):
differentiable.pyandNLPEvaluatorboth buildconstraint_fnsvia[c for c in model._constraints if isinstance(c, Constraint)](same filter/order), and the evaluator ravels withreshape(-1)— somultipliersaligns row-for-row. For scalar constraints the new code is identical to the old.Five sites had the pattern, all fixed:
differentiable_solve,_compute_sensitivity_at_solution,solve_fn_jvp(custom-JVP),implicit_differentiate, anddifferentiable_solve_l3. The issue named only the L1 path, but the L3 path crashed identically — a regression test caught it (my first pass missed two sites; the test exposed them).Tests
test_differentiable_vector_constraints.py(6 tests):(3,)+(2,)and equal-shape(3,)+(3,)no longer crash, on both the L1 and L3 paths;d(obj)/dp = −2.1, exact match) — guarding both the crash and a silent multiplier-ordering regression (which would return a wrong gradient, not crash);Full differentiable suite (78) passes; ruff/mypy clean.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Lq9NRuyCAWVxWoFv6AFuAt