projection: unit-aware smoothing_length API on the four Projection classes#200
Open
lmoresi wants to merge 1 commit into
Open
projection: unit-aware smoothing_length API on the four Projection classes#200lmoresi wants to merge 1 commit into
lmoresi wants to merge 1 commit into
Conversation
…asses
The L2-projection solvers solve
u - ∇·(α ∇u) = ũ
i.e. a screened-Poisson smoother whose Green's function decays as
exp(-r/L) with L = √α — equivalent in effect to a Gaussian convolution
of width L, obtained by one elliptic solve without ever forming the
kernel.
`smoothing` (= α, units length²) was the only existing knob and is
historically used as a tiny *numerical* regulariser (e.g. 1e-6), which
gives sub-grid L and no physical smoothing. This change adds a
parallel L-valued, unit-aware `smoothing_length` accessor on:
- SNES_Projection
- SNES_Vector_Projection
- SNES_Tensor_Projection (inherits)
- SNES_MultiComponent_Projection
Setter accepts a plain float, a Pint Quantity with length units, or
any unit-aware object understood by uw.non_dimensionalise; the value
is non-dimensionalised, squared and stored in `self._smoothing`, so
the two views stay consistent. Getter returns a Pint Quantity in
length units when a scaling context is set, else the plain ND float.
Class docstrings have been rewritten to put `smoothing_length` in
its mathematical context (screened-Poisson form, Green's function,
attenuation as 1/(1+k²L²), guidance on choosing L relative to the
cell size). Vector / Tensor / MultiComponent classes defer the full
derivation to SNES_Projection.
A new tests/test_0505_projection_smoothing_length.py locks:
- L → α = L² round-trip on all four classes
- α → smoothing_length = √α round-trip
- End-to-end: smoothing a step function at increasing L shrinks
the peak-to-peak range monotonically (screened-Poisson low-pass
behaviour)
Existing projection tests pass unchanged.
Underworld development team with AI support from Claude Code
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds a new, physically meaningful unit-aware smoothing_length accessor across the Projection solver family, intended as a length-scale view of the existing smoothing (α) coefficient with the convention α = smoothing_length². It also expands class/property docstrings to document the screened-Poisson interpretation and introduces a new test module to lock the API.
Changes:
- Adds
smoothing_lengthproperty (getter/setter) toSNES_Projection,SNES_Vector_Projection, andSNES_MultiComponent_Projection(and therefore toSNES_Tensor_Projectionvia inheritance), keepingsmoothingandsmoothing_lengthconsistent. - Updates projection class/property docstrings with a screened-Poisson / Helmholtz derivation and guidance on choosing the filter scale.
- Adds a new test module to check round-trips and end-to-end monotone smoothing behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
src/underworld3/systems/solvers.py |
Adds smoothing_length API and substantial docstring updates for projection solvers. |
tests/test_0505_projection_smoothing_length.py |
New tests for round-trip behavior and a basic end-to-end smoothing monotonicity check. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+2216
to
+2224
| except Exception: | ||
| # Fall back to magnitude-or-float coercion if the | ||
| # value doesn't carry/expect units. | ||
| if hasattr(L, "magnitude"): | ||
| L_nd = L.magnitude | ||
| else: | ||
| L_nd = L | ||
| self._smoothing = sympify(L_nd) ** 2 | ||
|
|
| except (TypeError, ValueError): | ||
| return sympy.sqrt(s) | ||
| if sval < 0: | ||
| return None |
Comment on lines
+27
to
+38
| def test_scalar_smoothing_length_roundtrip(mesh): | ||
| """L=0.05 ⇒ α=L²=0.0025; reading back gives L (unit-aware).""" | ||
| V = uw.discretisation.MeshVariable( | ||
| "V_sl", mesh, vtype=uw.VarType.SCALAR, degree=2, continuous=True) | ||
| proj = sys.Projection(mesh, V) | ||
| proj.smoothing_length = 0.05 | ||
| assert float(proj.smoothing) == pytest.approx(0.0025) | ||
| L = proj.smoothing_length | ||
| # In an active scaling context this is a Pint Quantity in metres; | ||
| # without one it's a plain float — both should round-trip. | ||
| L_num = getattr(L, "magnitude", L) | ||
| assert float(L_num) == pytest.approx(0.05) |
Comment on lines
+2432
to
+2433
| self._smoothing = sympify(L_nd) ** 2 | ||
|
|
Comment on lines
+2768
to
+2776
| try: | ||
| L_nd = uw.non_dimensionalise(L) | ||
| except Exception: | ||
| if hasattr(L, "magnitude"): | ||
| L_nd = L.magnitude | ||
| else: | ||
| L_nd = L | ||
| self._smoothing = sympify(L_nd) ** 2 | ||
|
|
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.
@benknight — this is the bit you asked me about: a length-scale-aware way to set the smoother on
Projection(and friends) instead of the unit-lesssmoothingknob. Splitting it out of a larger meshing branch so it can land independently.Summary
Adds an L-valued, unit-aware
smoothing_lengthaccessor to the four projection classes:SNES_ProjectionSNES_Vector_ProjectionSNES_Tensor_ProjectionSNES_MultiComponent_ProjectionThe existing
smoothingknob is preserved unchanged (units of length², historically used as a tiny numerical regulariser);smoothing_lengthis a parallel view with the physical conventionsmoothing = smoothing_length². The two stay consistent: setting one updates both.Why — the mathematics
The L2-projection solvers all minimise a Tikhonov-regularised misfit
whose Euler–Lagrange equation is the screened-Poisson equation
The free-space Green's function of$\bigl(1 - \alpha\nabla^2\bigr)$ decays as $\exp(-r/L)$ where
so the projection acts as a Gaussian-like convolution of width$L$ , obtained implicitly by one elliptic solve — no kernel is ever assembled. In Fourier space the transfer function is
so features at scale$\ll L$ are damped, features at $\gg L$ pass through, and features at $\sim L$ are attenuated by ≈ 1/2.
This makes$L$ — not $\alpha$ — the natural physical knob: you ask for "smooth this field at scale $L$ " and the solver delivers it.
API
Identical surface on
Vector_Projection,Tensor_Projection, andMultiComponent_Projection. The class docstrings now carry the screened-Poisson derivation (with guidance:L ≳ his needed for any real filtering;L ≈ 1–2·his a useful light de-noising default).Test plan
tests/test_0505_projection_smoothing_length.pyis new and locks:L → α = L²round-trip on all four classesα → smoothing_length = √αround-tripLmonotonically shrinks the projected field's peak-to-peak range (the screened-Poisson low-pass behaviour the docstring promises)tests/test_0504_projections.pyandtests/test_multicomponent_projection.pypass unchangedUnderworld development team with AI support from Claude Code