Barr & Houseman faulted-medium analytic solution in uw.analytic - #550
Barr & Houseman faulted-medium analytic solution in uw.analytic#550lmoresi wants to merge 5 commits into
Conversation
The linear (n=1) plane-strain solution for a fault terminating inside a viscous medium, from the Appendix of Barr & Houseman 1996 (GJI 125, 473-490). Implementation follows that of @gthyagi, who has been using this solution for fault benchmarking. Why it is worth having: every fault result we have is currently measured against another discretisation — a gmsh-union ribbon control — so there is no absolute standard. This is one, and it has a fault in it: an internal boundary carrying zero shear traction, continuous normal velocity and continuous normal stress, with the tip inside the medium. The structure is the interesting part. In polar coordinates about the tip, the stream function separates into a Fourier series in m = q/2: whole-integer m is continuous deformation, half-integer m IS the fault discontinuity, and boundedness at r=0 admits only one negative index, m = -1/2. That single mode carries the whole singularity, which is why slip goes as sqrt(r) and stress as 1/sqrt(r). The exponents are a property of the fault's own Fourier mode rather than an assumption, and the test asserting the slip is carried entirely by that mode says so. The tests verify the field IS a Stokes solution rather than comparing it to a stored answer: div u = 0, the momentum balance vanishes identically in both components, zero shear traction on both faces of the fault, normal velocity continuous across it, and slip = 2 U0 sqrt(r/R0) — the paper's own normalisation. If those hold together the transcription is the solution, whatever a solver later does with it. That mattered here. The half-integer sine terms of u_theta appear with one sign in the paper's boundary datum (A8b) and the opposite sign in its solution (A9b). Incompressibility settles it: for u_r = A sqrt(R) f and u_theta = sqrt(R) g, div u = 0 forces g' = -(3/2) A f, which is (A8b)'s sign. Verified by flipping it — the Stokes test fails with a divergence of (0.75 cos(t/2) + 2.25 cos(3t/2))/sqrt(r). Plane strain only. The paper's thin-viscous-sheet solution (A10-A13) has non-zero in-plane divergence and is a different equation set from our incompressible Stokes, so it is not a benchmark for this solver. Not yet wired to a UW3 solve: the analytic domain is a disc with the fault running from the centre to the perimeter, and 2-D faults cannot reach a boundary yet (#549). The solution and its verification stand on their own until that lands. Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
Pull request overview
Adds a new analytic benchmark solution (Barr & Houseman 1996, plane strain, Newtonian n=1) to uw.function.analytic, with tests that symbolically verify the expressions satisfy incompressible Stokes and the fault interface conditions. This provides an absolute (closed-form) reference for fault benchmarking, independent of discretisation.
Changes:
- Introduces
BarrHousemananalytic solution implementation (SymPy form + NumPy evaluator) and re-exports it underunderworld3.function.analytic. - Adds a focused test suite that verifies the solution by symbolic PDE/traction checks plus branch-cut/slip behaviour checks.
- Adds an import hook in the Cython analytic module to keep a single public analytic namespace.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/test_0210_barr_houseman_analytic.py | New symbolic + numeric verification tests for the Barr–Houseman fault-tip solution. |
| src/underworld3/function/analytic.pyx | Re-exports BarrHouseman into the existing uw.function.analytic namespace. |
| src/underworld3/function/_barr_houseman.py | Implements the Barr–Houseman plane-strain (n=1) analytic solution with SymPy expressions and a NumPy evaluator. |
Suppressed comments (2)
src/underworld3/function/_barr_houseman.py:211
slip()will currently returnnanfor negativerwithout an explicit error, which can hide caller mistakes (unlikeevaluate(), which validates its inputs). Adding a simple non-negativity check makes the API fail-fast and keeps downstream results from silently becomingnan.
r = np.asarray(r, dtype=float)
return 2.0 * self.U0 * np.sqrt(r / self.R0)
src/underworld3/function/_barr_houseman.py:188
- The closed-form expressions are duplicated in both
_polar()(SymPy) andevaluate()(NumPy). Even with the cross-check test, this is a drift hazard (a future edit might update one side only). Consider extracting the shared formula into a single helper that takes{sin, cos, sqrt}callables (or lambdifying the SymPy expressions once and caching the resulting NumPy callables) so there is only one source of truth.
u_r = (self.U0 / 4) * (
R**2 * (np.sin(t) - np.sin(3 * t))
- R**3 * (2 * np.sin(2 * t) - 2 * np.sin(4 * t))
+ np.sqrt(R) * (np.cos(t / 2) + 3 * np.cos(3 * t / 2))
)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # ------------------------------------------------------------------ sympy | ||
| @property | ||
| def symbols(self): | ||
| """The polar symbols ``(r, theta)`` the expressions are written in.""" | ||
| return sympy.symbols("r theta", positive=True) | ||
|
|
Adversarial reviewWe tried to break the transcription independently rather than re-running its own tests. Two coverage gaps found, both of which we closed by independent verification — the solution itself survives. 1. The symbolic Stokes check pins U0 = R0 = eta = 1, so a parameter-scaling error in the pressure would pass. A transcription with, say, a missing R0 power in 2. The PR text claims three fault conditions; the tests assert two. Zero shear traction and normal-velocity continuity are tested; normal-stress continuity is claimed and never asserted. We checked it: with the extension-positive convention, 3. tier_a on a new test file. The testing doctrine reserves tier A for explicitly reviewed tests. These are deterministic symbolic identities, and with this review plus the two additions above we would endorse the marking; without them it is self-assigned. What we could not fault: the branch-cut test is a genuine negative control (a bare Cross-PR note: the disc-with-daylighting-fault configuration this ultimately drives needs #552 (ribbon outcrop, now green) and #553 (transversal slit) — the interior verification here stands alone, as the description says. Underworld development team with AI support from Claude Code |
Both from the adversarial review on #550. The symbolic Stokes and fault-condition checks pinned U0 = R0 = eta = 1, so a parameter-scaling error could pass. Demonstrated rather than assumed: give the singular pressure term the wrong power of R0 and the unit-parameter momentum residual is exactly zero, while the same transcription with the parameters free gives U0 eta (1 - R0) cos(3 theta / 2) / (2 sqrt(R0) r^(3/2)) The checks now run with the parameters symbolic, which costs nothing and is a strictly stronger statement. BarrHouseman accepts SymPy parameters for the symbolic paths and refuses them, naming the reason, on the numeric ones. The paper states three fault conditions and the tests asserted two: normal-stress continuity was claimed in the description and never checked. It holds — with the extension-positive convention, sigma_tt = 2 eta e_tt + p has identically zero jump across the fault — and is now asserted, so the claim is load-bearing. Underworld development team with AI support from Claude Code
|
Both findings accepted and fixed in 006d0ae. Thank you — the first one was a real blind spot, not a theoretical one. 1. Parameter scaling. We did not take this on faith; we built the corruption you described and measured it. Giving the singular pressure term the wrong power of
So the test was blind in exactly the way you said. 2. Normal-stress continuity. Correct — claimed in the description, asserted nowhere. Now asserted, and it holds as you found: with the extension-positive convention 3. tier_a. Taking the endorsement as conditional on the two above, which are now in. If you would rather it sat at tier_b until @gthyagi has compared it against his implementation, say so and we will move it — the marking is not load-bearing for anything else. On the cross-PR note: #552 and #553 are better news than we knew when this was opened. The description's "not yet wired to a solve" paragraph is now the only thing standing between this and the full manufactured benchmark, and it stops being true as soon as #552 lands and #553 is decided. We have left the paragraph as-is rather than pre-announcing a capability that has not merged. Underworld development team with AI support from Claude Code |
Review finding on #550. `theta` carried `positive=True` while the fault conditions are checked at `theta = 0`, which that assumption excludes — SymPy would be within its rights to simplify a substitution the assumption says cannot occur. It is now `real=True`; `r` stays positive, which is honest since the solution is singular at the origin. The symbols were also rebuilt on every property access, so identity across calls depended on SymPy's global symbol cache rather than on us. Cached on the instance instead. Underworld development team with AI support from Claude Code
|
I independently verified the analytical solution on current head The important transcription point is slightly stronger than the PR description: BH96 equation A8b has
BH96 is an extension of BH92, but its Appendix disc is a separate exact The pressure sign in the PR also matches the papers' extension-positive convention; it must be negated only when comparing with a compression-positive numerical pressure. I built the current branch as an isolated macOS arm64 wheel and ran an independent SymPy verification script plus Result: 12 passed. I have verified the analytical implementation. |
|
I have verified only the analytical solution for BH96 so far. I’m currently building the numerical model and will compare its results with the analytical solution. Please keep this PR open until I complete the comparison. |
|
Follow-up: I also verified this against a numerical UW3 Stokes model with an actual mesh and MeshVariables. Because Numerical setup:
All three solves converged ( The pressure sign is also confirmed numerically: using the required opposite sign gives the convergent errors above, while comparing UW3 pressure directly with the paper's extension-positive pressure gives approximately 199% error at every resolution. Conclusion: the Mesh/MeshVariable Stokes solution converges to the PR's analytic field. This validates the analytical implementation numerically, but it does not claim that the current |
…ot both Every term of the velocity carries a positive power of r, so the limit at the fault tip exists and is zero. Refusing r = 0 for the velocity was over-strict, and it bit immediately: a mesh node lands exactly on the tip in any benchmark that puts the tip inside the domain, which is the whole point of the solution. evaluate_velocity now accepts the tip and returns zero there; evaluate_pressure refuses it and says why, since the pressure carries the r^(-1/2) term of the m = -1/2 mode and genuinely diverges. evaluate() returns both and inherits the pressure's refusal. Underworld development team with AI support from Claude Code
Prompted by Louis: a benchmark boundary does not have to be pinned everywhere, and leaving the normal component free on one wall is worth doing. With velocity Dirichlet on EVERY wall the pressure is determined only up to a constant, and the datum must additionally carry exactly zero net flux; a traction condition on one component removes both requirements rather than patching them. It is also what the paper does — Barr & Houseman's left-hand boundary carries a constant normal stress, not a prescribed normal velocity. evaluate_traction returns sigma . n with sigma = tau + p I, extension positive as the paper has it, refusing the tip where the stress diverges. The stress is built from SymPy-derived strain rates, lambdified once and rotated into Cartesian. The test is a cross-check rather than a restatement: it arrives at the zero-shear fault condition by this different route, with a negative control off the fault. Underworld development team with AI support from Claude Code
The linear (
n = 1) plane-strain analytic solution for a fault terminating inside a viscous medium, from the Appendix of Barr & Houseman 1996 (GJI 125, 473–490).Implementation follows that of @gthyagi, who has been using this solution for fault benchmarking — hence the review request. A second independent implementation is the strongest check available on this, so please do compare against yours.
Why
Every fault result in the current campaign is measured against a gmsh-union ribbon control — that is, against another discretisation. There is no absolute standard. This is one, and it has a real fault in it: an internal boundary carrying zero shear traction, continuous normal velocity and continuous normal stress, with the tip inside the medium.
The structure is the interesting part
In polar coordinates about the tip, the stream function separates into a Fourier series in
m = q/2:m— continuous deformationm— the fault discontinuity itselfand boundedness of the velocity at
r = 0admits only one negative index,m = -1/2. That single mode carries the entire singularity, which is why slip goes assqrt(r)and stress as1/sqrt(r). The exponents are a property of the fault's own Fourier mode rather than an assumption, and one of the tests asserts exactly that — drop the half-integer term and the slip vanishes.The tests verify it IS a solution
Not a comparison against a stored answer, and not against a UW3 solve — symbolic checks that the field satisfies the equations:
div u = 0eta lap(u) + grad(p)0in both componentstau_r_theta = 0attheta = 0and2 piu_thetaequal on both faces2 U0 sqrt(r/R0), so2 U0at the perimeter — the paper's own anchorIf those hold simultaneously the transcription is the solution, whatever a solver later does with it.
A transcription trap, settled by mathematics
The half-integer sine terms of
u_thetaappear with one sign in the paper's boundary datum (A8b) and the opposite sign in its solution (A9b), and the same mismatch appears in the plane-stress pair (A12b vs A13b).Incompressibility settles it without anyone adjudicating a scanned minus sign: for
u_r = A sqrt(R) f(theta)andu_theta = sqrt(R) g(theta),div u = 0forcesg' = -(3/2) A f, which integrates to (A8b)'s sign. Verified by flipping it — the Stokes test then fails with a divergence of(0.75 cos(t/2) + 2.25 cos(3t/2))/sqrt(r), non-zero purely in the fault modes.@gthyagi — this is the specific point worth checking against your implementation.
Scope and conventions
d_j tau_ij + d_i p = 0. The module follows the paper and documents it; negate to compare against a compression-positive solver.B = 2 eta_0atn = 1(their eq 1), sotau = 2 eta edotas usual.arctan2(y, x) mod 2*pi. A bareatan2would put it on the negative x axis and silently return the wrong face; there is a test for that.Not yet wired to a solve
The analytic domain is a disc with the fault running from the centre to the perimeter — the fault reaches the boundary, which 2-D UW3 cannot do yet (#549). The solution and its verification stand on their own until that lands, and the benchmark can be connected immediately afterwards.
Tests:
test_0210_barr_houseman_analytic(7).test_1015_analytic_solcxandtest_1062_constrained_solcxpass unchanged (12 total); style gate clean.Underworld development team with AI support from Claude Code