Skip to content

Stack optimizer constraints so SLSQP differentiates them once per point - #42

Merged
Ickaser merged 3 commits into
LyoHUB:mainfrom
SECQUOIA:upstream-fix/slsqp-constraint-stacking
Aug 6, 2026
Merged

Stack optimizer constraints so SLSQP differentiates them once per point#42
Ickaser merged 3 commits into
LyoHUB:mainfrom
SECQUOIA:upstream-fix/slsqp-constraint-stacking

Conversation

@bernalde

@bernalde bernalde commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Closes #41.

Each component of the constraint system was its own entry in the cons tuple, so Eq_Constraints computed all four residuals on every call while each lambda discarded three, and SLSQP finite-differenced each entry separately — a full 8-point sweep per entry on a 7-variable problem instead of one sweep for the whole system. dry() solves one such problem per timestep, so the overhead landed on every simulated hour.

This stacks the equality residuals into one vector-valued constraint and the inequality residuals into another, and supplies the exact gradient of the linear objective.

Results

Standard 5% mannitol fixtures from tests/conftest.py, median of 3 runs:

opt_Tsh opt_Pch opt_Pch_Tsh
current 11.94 s 1.26 s 13.42 s
stacked constraints 4.61 s 0.48 s 5.55 s
stacked + exact objective gradient 3.48 s 0.37 s 4.14 s
speedup 3.4× 3.4× 3.2×

Output trajectories are bitwise identical to the current code on all three cases — same shape, np.array_equal true across all seven columns (time, Tsub, Tbot, Tsh, Pch, flux, percent dried). The objective x[0]-x[4] is linear, so its finite-difference gradient was already exact to the last bit; supplying it analytically only removes the evaluations.

What is deliberately not here

I also tried memoizing the stacked constraint functions by x. It made no measurable difference — 3.49 s vs 3.48 s on opt_Tsh — because with the constraints stacked there is one finite-difference sweep per point and each evaluation in it is at a different x, so there is nothing to reuse. It is not in this PR.

lyopronto/functions.py is untouched; only the three optimizer modules change.

Tests

pytest tests/ on this branch: 144 passed, 1 skipped, coverage 99%.

The suite itself is independent corroboration: 191 s on this branch against 608 s on the parent commit, same machine, same suite.

Branched from 4fb63df. Independent of #38 and #40, which touch different modules.

Each component of the constraint system was its own entry in the cons tuple,
so Eq_Constraints computed all four residuals on every call while each lambda
discarded three, and SLSQP finite-differenced each entry separately -- a full
8-point sweep per entry on a 7-variable problem instead of one sweep for the
system. dry() solves one such problem per timestep, so the overhead landed on
every simulated hour.

Stack the equality residuals into one vector-valued constraint and the
inequality residuals into another, and supply the exact gradient of the linear
objective so SLSQP does not finite-difference that either.

Measured on the standard fixtures from tests/conftest.py, median of 3:

                    current   stacked   + objective gradient
  opt_Tsh           11.94 s    4.61 s   3.48 s   (3.4x)
  opt_Pch            1.26 s    0.48 s   0.37 s   (3.4x)
  opt_Pch_Tsh       13.42 s    5.55 s   4.14 s   (3.2x)

Output trajectories are bitwise identical to the previous code on all three
cases: same shape, np.array_equal true across all seven columns. The objective
is linear, so its finite-difference gradient was already exact; supplying it
analytically only removes the evaluations.

Full test suite: 144 passed, 1 skipped, in 191 s against 608 s on the parent
commit.
@Ickaser

Ickaser commented Aug 6, 2026

Copy link
Copy Markdown
Member

This falls into the category of things I knew should be fixed, but didn't bother since I thought they would be made obsolete once Pyomo comes into the picture. I want to double check that the analytical Jacobians are correct, though, since I thought there would be a little more nonlinearity here.

@Ickaser

Ickaser commented Aug 6, 2026

Copy link
Copy Markdown
Member

Figured out what felt odd: the Jacobian is of the objective, not of the constraints, and the objective is dead simple as formulated. I took the liberty of standardizing names as objfun and objfun_jac where 2 out of 3 modules were fun and fun_jac; once tests pass I will merge.

@Ickaser
Ickaser merged commit 2a5f2d4 into LyoHUB:main Aug 6, 2026
3 checks passed
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.

Optimizers re-solve the constraint system once per component, and once per component per gradient sweep

2 participants