Stack optimizer constraints so SLSQP differentiates them once per point - #42
Merged
Ickaser merged 3 commits intoAug 6, 2026
Merged
Conversation
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.
This was referenced Aug 5, 2026
Closed
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. |
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 |
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.
Closes #41.
Each component of the constraint system was its own entry in the
constuple, soEq_Constraintscomputed 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_Tshopt_Pchopt_Pch_TshOutput trajectories are bitwise identical to the current code on all three cases — same shape,
np.array_equaltrue across all seven columns (time, Tsub, Tbot, Tsh, Pch, flux, percent dried). The objectivex[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 onopt_Tsh— because with the constraints stacked there is one finite-difference sweep per point and each evaluation in it is at a differentx, so there is nothing to reuse. It is not in this PR.lyopronto/functions.pyis 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.