Repository navigation
Conversation
mkeeler43
force-pushed
the
feat/2d-optistep-sensitivities
branch
from
September 3, 2026 17:14
aa4810d to
556637c
Compare
mkeeler43
force-pushed
the
feat/2d-optistep-sensitivities
branch
from
September 3, 2026 17:23
556637c to
6c8d174
Compare
mkeeler43
force-pushed
the
feat/2d-optistep-sensitivities
branch
from
September 3, 2026 17:41
6c8d174 to
755fd0a
Compare
beams2d recorded only the objective value and the design at each step. The sensitivity there, the move the optimizer made, and the objective that move bought were all computed and discarded, so recovering any of them afterwards meant re-running the solve that had already produced them. Each step now carries what the optimizer already knew: x the design the step was evaluated at x_sensitivities the filtered objective sensitivity there x_update the move taken from that design obj_values_update the objective change that move produced Nothing new is computed; every value already existed in the loop. The step is recorded after `inner_opt` rather than before the sensitivity block, since the move is not known until then, and the design is taken beforehand because the overhang filter rebinds `xPrint`. Three choices worth stating. `x_sensitivities` holds the objective sensitivity by itself, shaped like the design it belongs to, so there is one value per design variable. The volume sensitivity `dv` is not stacked alongside it. thermoelastic2d and beams3d do stack theirs, but consumers flatten this field, so an extra channel doubles its length with nothing recording that it did; photonics2d, the other 2D problem reporting sensitivities, reports the objective gradient alone. `x_update` is measured in printed density, the space the recorded design is in, so `design + x_update` is the next step's design. `inner_opt` also returns the raw density field, and differencing that would mix the two spaces. The last step's `obj_values_update` stays None. thermoelastic2d fills its own in by running an extra iteration after convergence and reverting the design, which buys one scalar for the price of a full solve; the same value is derivable from the next step for every step that has one, and the revert would put beams2d's returned design at risk. `ExtendedOptiStep` and its `design` field are untouched; `x` holds the same array, so callers reading either keep working. Optimizer behavior is unchanged: step count, objective values, per-step designs and the returned design are identical to before, with and without the overhang constraint.
mkeeler43
force-pushed
the
feat/2d-optistep-sensitivities
branch
from
September 3, 2026 17:45
755fd0a to
cfa906e
Compare
This branch has not been deployed
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.
What
beams2drecorded only the objective value and the design at each optimizationstep. The sensitivity at that design, the move the optimizer made from it, and the
objective change that move produced were all computed and thrown away, so
recovering any of them afterwards meant re-running the solve that had already
produced them.
Each step now carries what the optimizer already knew:
xx_sensitivitiesx_updateobj_values_updateNothing new is computed — every value already existed in the loop. The step is
recorded after
inner_optrather than before the sensitivity block, because themove is not known until then, and the design is taken beforehand because the
overhang filter rebinds
xPrint.ExtendedOptiStepand itsdesignfield are untouched.xholds the same array,so callers reading either one keep working.
Three choices worth a reviewer's attention
x_sensitivitiesis the objective sensitivity alone, shaped like the design.One value per design variable.
thermoelastic2dandbeams3dstack theirs as[objective..., constraint]on a leading axis;photonics2dreports the objectivegradient bare. The bare form is used here because downstream consumers flatten this
field — EngiOpt's collector does
np.ravel(...)per step — so an extra channeldoubles the vector's length with nothing recording that it did, and a reader
assuming one sensitivity per design variable would misread it silently. The volume
sensitivity
dvis one line to add back if that is wanted.x_updateis in printed-density space.inner_optreturns three fields — rawdensity variables, processed density, printed density — and the recorded design is
the printed one. Differencing the raw field would mix the two spaces, so the move
is measured against the printed one, making
design + x_updateexactly the nextstep's design. There is a test for that.
The last step's
obj_values_updatestaysNone.thermoelastic2dfills itsown in by running an extra iteration after convergence and then reverting the
design. That buys one scalar for the price of a full solve, and the value is
derivable from the next step for every step that has one. On
beams2dthe revertwould also put the returned design at risk, since
xPrintis rebound three timesper iteration.
Effect on existing behavior
None. Running
optimizefor 25 iterations on a 20x10 grid, with and without theoverhang constraint, before and after:
mainvs branchTests
tests/test_beams2d.pyis new, followingtests/test_photonics2d.py(module-scopedfixture, small grid, few iterations; about a second to run). It checks step
numbering, that each recorded field is shaped like the design, that the sensitivity
is nonpositive and finite, that
design + x_updatelands on the next step's design,and that
obj_values_updatematches the next step's objective difference on everystep but the last.
If you add tests here:
max_itermust be passed tooptimize, not to theconstructor.
optimizerebuilds its config asConfig(**{**asdict(self.simulate_config), **config}), andSimulateConfigdoes notcarry
max_iter, so a constructor-supplied value is silently replaced by the defaultof 100.
Follow-ups this does not attempt
OptiStep.x_sensitivitiesis documented only as "the sensitivities of the designvariables". Across the problems that populate it, the layout varies — bare grid
(
photonics2d) against channel-first stacks of differing depth (thermoelastic2d4,beams3d2), with the channel order recorded nowhere and no relation toproblem.objectives. A metric written against this field cannot currently beproblem-agnostic. Worth a documented convention in
core.py, separately from this PR.heatconduction2dbuilds its history by regex-scraping an IPOPT log after adolfin-adjoint container run, with no per-iteration callback holding the design; it
needs a
derivative_cb_poston theReducedFunctionaland new arrays in the outputnpz.
mto2dparses solver output columns that contain no gradient.This changes what
optimize()returns at runtime and touches no published dataset.Dataset generation keeps the returned design and discards the history — see
photonics2d/dataset_slurm_test.py, where it is bound to_obj_trajectory— sotrajectories reach a dataset only if a generation script is changed to keep them.