Iteration-count reporting, interface-width docs, and a lattice-pinning guard - #14
Merged
Merged
Conversation
The --eta help and the comment at its default both said "two grid spacings"; the code has used one grid spacing since 820e95c (MAINT: eta = grid spacing). The README's API example passed well_weight= to PhaseFieldRegularization, which has no such argument (TypeError; the keyword is weight). It also pinned eta=1.0 -- an interface as wide as the whole unit cell, which collapses the design to constant density -- so drop it and take the one-grid-spacing default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-iteration lines, the objective/volume/CG histories and the dumped frames are all indexed by the number of *accepted* iterates, but the final `iters=` line, the `nb_iterations` attribute and the final frame's label used `info["nit"]` -- the optimizer's raw step count, which for the trust region also includes rejected trial steps. So `nb_iterations` disagreed with the length of the histories it is supposed to index, and the final frame carried a label from a different counter than every other frame. Mixing the two also made the final-frame duplicate check miss: it compared an accepted-count label against the raw count, so whenever the last iterate had already been dumped it was written a second time under a wrong label. Report the accepted count (so the final line agrees with the last per-iteration line), name the raw count in parentheses when the two differ, and record it separately as `nb_optimizer_steps`. Same fix in the conduction driver, which had the same conflation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Modica-Mortola energy of a flat interface is translation invariant in the
continuum; on a grid it is not, and once the interface is only a couple of
nodes wide the sub-pixel translation ripple becomes a barrier that stops
features moving at all. The interface width defaults to one grid spacing, so
it is worth knowing where that sits on the curve.
Measured relative barrier dE/E over one pixel of translation:
eta/h 0.50 0.75 1.00 1.50 2.00
dE/E 1.7e-2 3.7e-4 6.1e-6 1.0e-9 1.3e-13
At eta = h the critical depinning force is ~2e-4 in energy per unit volume,
against a curvature force sigma/R of 0.3-20 for any feature that fits in a
unit cell and a default --bfgs-gtol of 2.5 in the same units -- 3 to 4 orders
of margin, including at the end of a run where it is tightest. Confirmed
end-to-end: a half-pixel spectral shift of the initial density moves the
converged 64^2 objective by 2.2e-7 relative, the same as the integer-shift
control.
The eta = h/2 case is asserted to be pinned. That positive control is the
half that matters: without it the upper bounds would pass vacuously if the
energy ever stopped depending on the interface position at all.
Bounds are the worst case over element x quadrature with ~10x headroom, and
were cross-checked against a fully relaxed landscape (each sub-pixel position
re-minimized at fixed volume and fixed first moment); relaxed and analytic
agree to 2% at eta >= h, which is what licenses the cheap analytic profile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Three independent fixes that came out of investigating why the J-FFT
preconditioner appeared to underperform in muTopOpt runs. (The preconditioner
turned out to be fine — muGrid's and muFFTTO's J-FFT agree to 8 digits on the
same problem; the slow convergence is a property of sharp SIMP density fields.)
1.
DOC:interface-width default and README snippet--eta's help and the comment at its default both said two grid spacings;the code has used one since
820e95c(MAINT: eta = grid spacing).The README's API example called
PhaseFieldRegularization(homog, eta=1.0, well_weight=1e-3). There is nowell_weightargument — the snippet raisesTypeError. It also pinnedeta=1.0, an interface as wide as the whole unit cell, which collapses thedesign to constant density; dropped so the example takes the one-grid-spacing
default. Verified the snippet now runs end to end.
2.
BUG:accepted iterates vs raw optimizer stepsThe per-iteration lines, the histories and the dumped frames are indexed by
the number of accepted iterates. The final
iters=line, thenb_iterationsattribute and the final frame's label usedinfo["nit"]— theraw step count, which for the trust region also counts rejected trial steps.
Consequences:
nb_iterationsdisagreed with the length of the histories it ismeant to index, the final frame carried a label from a different counter than
every other frame, and the duplicate-frame check compared the two counters
against each other — so whenever the last iterate had already been dumped it
was written twice, under a wrong label.
Now:
nb_iterationsequals the history length,frame_iterationsis on one counterthroughout, and the raw count is kept as a new
nb_optimizer_stepsattribute.simulate_conduction.pyhad the same conflation and gets the same fix.This surfaced as
--bfgs-maxiter 150and400both reportingCONVERGENCEwith
itersequal to the budget and an identical objective. The root cause ofthat was a separate bug in NuMPI's trust-region driver, fixed in
muSpectre/NuMPI#94; with both in place the same run reports
iters=94 (117 trust-region steps)for either budget.3.
TST:lattice-pinning guardNew
test/test_interface_pinning.py(42 cases, 0.1 s, no optimizer). Sinceetadefaults to a single grid spacing, it is worth knowing how close that isto the Peierls regime where a diffuse interface can no longer move between
lattice sites. Relative barrier
dE/Eover one pixel of translation:At
eta = hthe critical depinning force is ~2e-4 in energy per unit volume,against a curvature force
sigma/Rof 0.3–20 for any feature that fits in aunit cell, and a default
--bfgs-gtolof 2.5 in the same (mesh-invariantvolume-fraction-derivative) units — 3 to 4 orders of margin, including at the
end of a run where it is tightest. Confirmed end to end: a half-pixel spectral
shift of the initial density moves the converged 64² objective by 2.2e-7
relative, the same as the integer-shift control.
eta = h/2is asserted to be pinned. That positive control is the half thatmatters — without it the upper bounds would pass vacuously if the energy ever
stopped depending on interface position. Bounds were cross-checked against a
fully relaxed landscape (each sub-pixel position re-minimized at fixed volume
and fixed first moment): relaxed and analytic agree to 2% at
eta >= h, whichis what licenses the cheap analytic profile in the test.
Mutation-tested rather than trusted: tightening the
eta = hbound to 1e-7fails 6 cases; making the energy shift-independent fails 18, the positive
control among them.
Checks
pytest— 160 passed (118 before this PR + 42 new)🤖 Generated with Claude Code