Uh oh!
There was an error while loading. Please reload this page.
Analytic solutions declare their own boundary conditions - #578
Conversation
The suite carried two mixins, FreeSlipWalls and FixedWalls, each looping over every boundary of a solution and applying one condition to all of them. That encodes "every boundary is a wall of the same kind" — true of the classical box benchmarks, false of what the suite has to serve next: a spherical shell or annulus with different conditions on the two radii, a faulted disc, a channel driven at one end. FreeSlipWalls would have had to grow a parameter for each of those, and inheritance advertises the choice as if it were part of what the solution is. Both mixins are gone. Every solution now writes apply_boundary_conditions itself and composes three module-level helpers, each taking the boundaries it applies to rather than reading self.boundaries: free_slip(solver, boundaries, normal=None) prescribed_velocity(solver, boundaries, velocity) prescribed_scalar(solver, boundaries, field) A solution needing two kinds of condition calls two of them. The scalar helper also removes the third copy of the same Dirichlet loop, which the transport and Richards families each carried. The pressure nullspace is now stated by the solution rather than set as a side effect of a wall type. It is a property of the domain — enclosed, so the pressure is fixed only up to a constant — and not of any one boundary's condition, and two different mixins both setting it hid that. free_slip takes normal= so a curved-geometry solution can ask for an analytic normal. CylindricalStokes, the one curved case here, keeps the geometric default deliberately: it is the direction the straight-facet boundary integral actually sees, and an analytic X/|X| would trade a machine-precision pressure gauge for a consistency error that grows with facet non-uniformity (#561). This is a pure refactor and it was verified rather than asserted. A recording mock solver captured the ordered sequence of calls and attribute sets that apply_boundary_conditions produces for every registered solution — sympy arguments by srepr, floats by their exact hex form — on the merge base and on this branch. The two recordings are byte-identical across all 21 cases (20 registered solutions plus both CylindricalStokes boundary cases). The equivalence check did expose one latent defect, left in place and marked with a TODO(BUG): CylindricalStokes with boundary="zero" does not remove the pressure nullspace, where every other enclosed case does. An annulus held at zero velocity on both arcs is enclosed, so its pressure is determined only up to a constant. Fixing it would change behaviour and belongs under its own test. Underworld development team with AI support from Claude Code
The subsystem note described the two mixins. Replace that with the composition the code now uses: a section on the three helpers, what each takes, why functions rather than mixins, why the pressure nullspace is the solution's statement and not a wall type's side effect, and which normal to use on a curved boundary. Also corrects the one stale cross-reference left in the conformance suite's comments, which still pointed at "the mixins". Underworld development team with AI support from Claude Code
Underworld development team with AI support from Claude Code
lmoresi
commented
Aug 15, 2026
Adversarial reviewWe went looking for behaviour this refactor changes without saying so, and for places where the new shape is weaker than the one it replaces. The equivalence claim holds, and it is the right claimThe recording mock captures the ordered call sequence with sympy compared by Worth stating explicitly, because deleting a base class is not usually free: both mixins carried exactly one method each and no attributes, so removing them from the MRO cannot change anything except Findings1. 2. 3. The asymmetry between the two Dirichlet helpers is unexplained in the code. None of these change a result. The defect the check exposed
We note what made it findable. Under the mixins the nullspace was set inside a wall type, so the zero-slip branch's early Coverage
|
The normal test asserted `registered["Left"] is None`, which passes because add_rotated_freeslip_bc's own default happens to be None. The claim being made is that free_slip does not pass `normal` when none was chosen, so record the call instead; a separate test confirms the recorded call is one a real Stokes solver accepts. Also widen prescribed_velocity's documented argument type, which excluded the one in-tree caller that passes a tuple, and say why prescribed_scalar brackets its argument where prescribed_velocity does not. Underworld development team with AI support from Claude Code
lmoresi
commented
Aug 15, 2026
All three review findings are addressed in df1b7b2. Finding 2 was the one worth acting on. assertsolver.calls== [
(0.0, "Left", {}),
(0.0, "Right", {"normal": radial}),
]That is the claim the helper actually makes — not passing Findings 1 and 3 are docstrings: Equivalence re-verified after the edits — the recording is byte-identical to the merge-base baseline, 119 lines, |
Uh oh!
There was an error while loading. Please reload this page.
…itions (#578) Underworld development team with AI support from Claude Code
#578 replaced the boundary-condition mixins with composed functions, so the refusal's explanation now names what the solution actually needs — a component-wise Dirichlet condition on an internal boundary — rather than the two classes that no longer exist. Underworld development team with AI support from Claude Code
Analytic solutions declare their own boundary conditions
The ruling, and why
uw.analyticshipped with two boundary-condition mixins:Each loops over every boundary of the solution and applies one condition
to all of them. That encodes "every boundary is a wall of the same kind". It is
true of the classical box benchmarks the suite started with, and false of most of
what it has to serve next — a spherical shell or annulus with different
conditions on the two radii, a faulted disc, a channel driven at one end.
FreeSlipWallswould have had to grow a parameter for each of those geometries,and inheritance advertises the choice as if it were part of what the solution
is rather than something it decides.
The ruling: delete the mixins, keep the shared bodies as functions.
What replaced them
Every solution now writes its own
apply_boundary_conditionsand composes threemodule-level helpers in
analytic/_base.py. Each takes the boundaries itapplies to — an explicit list, not
self.boundaries:A Velic solution is then three lines, and the composition is a choice the reader
can see rather than something a base class does on its behalf:
Per-boundary granularity is the whole point, and it now costs nothing:
Three further things fall out of this:
The pressure nullspace is now a statement, not a side effect. It is a
property of the domain — enclosed, so the pressure is determined only up to a
constant — and not of any one boundary's condition. Two different mixins both
setting it hid that. Each solution now says
solver.petsc_use_pressure_nullspace = Truefor itself, one line, in view.The third copy of the Dirichlet loop is gone.
_Transportand_Gardnereach carried their own scalar version because neither Stokes mixin fitted a
scalar solution; both now call
prescribed_scalar.free_sliptakesnormal=. A curved-geometry solution can now ask for ananalytic normal.
CylindricalStokes— the one curved case in the suite — keepsthe geometric default, and that is now written down as deliberate rather than
left as an omission: per #561 the geometric normal is measure-weighted to match
the straight-facet integral the assembler evaluates, so the constant pressure
stays a null vector to machine precision, where
X/|X|is exact for the truecircle but keeps a consistency error that grows with facet non-uniformity. See
"Which normal to use" in
docs/developer/subsystems/rotated-freeslip.md.The equivalence evidence
This is a pure refactor, and it was verified rather than asserted.
A recording mock solver stands in for the solver and captures the ordered
sequence of method calls and attribute sets that
apply_boundary_conditionsproduces — sympy arguments compared by
srepr, floats by their exact hex form,so the recording is a fingerprint and not a pretty-printed approximation. It was
run over every registered solution on the merge base and again on this branch:
assesspresent, soCylindricalStokesis covered) plus both of itsboundary="free"/boundary="zero"branches, which are the only case in the suite where theconditions depend on a constructor argument.
diffof the two recordings: empty. Byte-identical, every solution.What the equivalence check exposed
One latent defect, left in place, filed as #577 and marked with a
TODO(BUG)at the exact location (
analytic/kramer.py,CylindricalStokes.apply_boundary_conditions):CylindricalStokeswithboundary="zero"returns without removing the pressurenullspace, where every other enclosed case in the suite removes it. An annulus
held at zero velocity on both arcs is enclosed, so its pressure is determined
only up to a constant, and a direct solve on the singular saddle can return a
quiet, wrong answer — which is precisely the failure this suite exists to catch.
It is preserved here because this PR is behaviour-preserving by contract; the fix
changes an answer and belongs under its own regression test.
The old code made this easy to miss: the nullspace was set inside the free-slip
branch, so the zero-slip branch's
returnskipped it. With the nullspace statedby the solution rather than buried in a wall type, the omission is visible on the
page.
No deprecation is owed
__init__.pyre-exportedFreeSlipWallsandFixedWalls; both are removedoutright, with no shim. The package landed on 2026-08-15 as #571, so there are
no external users to break.
free_slip,prescribed_velocityandprescribed_scalartake their place in__all__— a solution written outsidethis package has to be able to reach the helpers it composes, so they are public
API rather than a private convenience (Style Charter §6, no deep-import-only
features).
Composes with the Barr & Houseman work
feature/pr550-integrate(#550, not yet merged) has anapply_boundary_conditionsthat refuses, with a message naming #549. Under the mixins that was an
exception to the pattern; after this refactor it is the ordinary shape — a
solution stating its own boundary conditions, which in that case is "not yet
these". It composes cleanly and needs only a trivial rebase (its class declaration
does not name either mixin).
Tests
tests/test_1016_analytic_contract.py— the two mixin tests are rewrittenagainst the helpers, and three tests are added: that two boundaries can carry
different conditions (the reason for the change), that
normal=reachesthe solver when given and is absent when not, and that the helpers are exported.
test_1015–test_1028): 309 passed, 4m13s.tests/analytic_full/— the whole family, every gate: 189 passed, 9m11s.scripts/test.sh --p 2end to end, serial batches plus np=2: 2163 passed,0 failures, 0 errors, exit 0.
Underworld development team with AI support from Claude Code