Integration-point variables and a semi-Lagrangian history at the integration points - #703
Conversation
Adds create_delta_fe(quad, polytope): a PetscFE whose basis is the identity on the mesh quadrature rule, built from a UW3-registered PetscSpace type "uwdelta" (uw_delta_space.h, PetscSpaceRegister) and a PETSCDUALSPACESIMPLE dual space with one point-evaluation functional per rule point. A field of this type in the auxiliary DM is read by the pointwise functions as a[] with no interpolation: the intended carrier for the semi-Lagrangian history (values injected at the integration points, never sampled elsewhere). All dofs sit on the cell, so the local vector is (ncells, Nq). PETSc's own PETSCSPACEPOINT is the same idea but errors unless tabulated at exactly its own points in its own order, which breaks PetscFESetUp (one point per functional), face tabulation in PetscDSSetUp and boundary integrals over auxiliary fields; the plugin type returns zeros off its rule instead and needs no PETSc patch, so stock conda PETSc works. tests/test_0064: identity tabulation on all four cell types, cell-only section layout, zeros off-rule and permuted identity as negative controls. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
…on points A peer of MeshVariable and swarm variables, built on the quadrature-point element (create_delta_fe) on mesh.integration_rule, so the pointwise functions read its values at the integration points with no interpolation. One value per rule point per cell; cell_data views (ncells, Nq, 1); coords are the assembler's own integration points (DMPlexComputeCellGeometryFEM). evaluate() is defined as the nearest integration point of the owning cell - the only extension under which a query agrees with what the assembler used at that point; exterior points take the nearest point on the rank. The JIT refuses derivatives of the symbol (the tabulated gradient is zero, so a derivative would be a silent zero), and solvers check their element's rule against the mesh rule when they attach the auxiliary vector (off-rule the field reads as zero). Base class hooks: _create_petsc_fe and _basis_key on _BaseMeshVariable; the mesh coordinate cache keys on _basis_key; EnhancedMeshVariable takes its storage class from _base_variable_class. tests/test_0065: layout on triangle/tet/quad; integral of random point data equals the hand quadrature sum (with a one-point perturbation control); P2 projection of P2 point data exact to solver tolerance; evaluate exact at own points and equal to the nearest-in-cell rule elsewhere (a P1 interpolant does not match, as the control); both guards. Docs: docs/developer/subsystems/integration-point-variables.md. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
The history slots are IntegrationPointVariables, so the advected value the weak form sees at each integration point is the solution from k+1 steps ago evaluated exactly at the departure point of that integration point. No nodal history field, no second interpolation. A delta field cannot be sampled off its points, so the slot-to-slot chain of SemiLagrangian is replaced by nodal snapshots of the solution and velocity at the last 'order' times; slot k is filled by tracing k+1 RK2 segments back from every integration point (segment j with the velocity at time n-j and that step's dt) and evaluating the snapshot from time n-k at the foot. Each slot carries one evaluation error rather than one per generation. Drops in as DuDt for AdvDiffusion; the flux history DFDt stays nodal. tests/test_0066: for a P2 field in a uniform velocity both slots reproduce the exact departure-point values to 1e-12 (the nodal scheme does not, as the control); rotating Gaussian is at least as accurate as nodal SLCN and keeps the peak. Measured at cellSize 0.05, half a revolution: Courant 1 L2 3.6e-3 -> 3.2e-3, peak 0.987 -> 0.9998; Courant 2 equal (dt-dominated). Scalar only; no ALE, no checkpoint state. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
…ments The least-squares fit of the sampled departure-point values onto the continuous space is a per-cell interpolant through interior points when the rule has as many points as the element has local dofs (P2 on a triangle at qdegree 2), and a mode then grows ~1.1x per step at small Courant number (rotating Gaussian, C=0.25: flat for 75 steps, then blow-up; zero-velocity control stationary). At 2x the points (qdegree 3) the fit is contractive and the scheme is stable through a full revolution. The constructor now raises at <= 1x and warns below 2x. Adds a monotone_mode pass-through. Measured at cellSize 0.05, qdegree 3, half revolution, L2 nodal -> IP: C=0.25 1.30e-2 -> 6.3e-4; C=0.5 3.55e-3 -> 9.0e-4; C=1 3.62e-3 -> 3.24e-3; C=2 equal. Integral of T^2 over 63 steps at C=0.5: nodal -1.4%, IP -0.03%. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
One-step growth factors (P2, cellSize 0.1, Courant 0.25). Pure advection: nodal SLCN 0.9989; integration-point 6 points 1.028, 9 points 1.005, 12 points 1.0003. With physical diffusion at cell Peclet 100: 6 points 1.0095, 9 and 12 points 0.996. The integration-point map is never strictly contractive under pure advection (it does not dissipate; the nodal scheme's 0.999 is its numerical diffusion) and oversampling brings it to neutral; with diffusion, 1.5x and 2x oversampling are stable and 1x is not. The conical 9-point rule ran a full revolution bounded (energy +0.07 %, saturating). Guard unchanged (raise at <= 1x, warn below 2x); the warning now states the measured behaviour. Docs carry the corrected mechanism: Galerkin with a sampled load is the weighted least-squares fit, the shifted field has sub-cell kinks, and the growth is aliasing of the sampled norm. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
The RK2 trace is second order only if the mid-point velocity is the
velocity at t^{n+1/2}. Both SemiLagrangian and IntegrationPointSemiLagrangian
now take it there: on the current interval by extrapolation from the two
most recent velocity fields, 1.5 v^n - 0.5 v^{n-1} (v^{n+1} is not known
when the history is built), and on the older segments of a multi-step
integration-point history by the average of the two known ends. With v^n
alone the foot is off by b dt^2/2 in a flow accelerating at rate b.
SemiLagrangian keeps the previous velocity in a managed v_prev mesh
variable recorded after each trace (v^n alone on the first step);
_velocity_nd_at takes an optional expression and now accepts a mesh
variable as V_fn. The integration-point scheme keeps at least two
velocity snapshots.
tests/test_0066: uniformly accelerating uniform flow, both schemes hit
the exact foot (integration-point to 1e-5, bounded by an evaluator edge
case on one foot in ~2000; nodal to 1e-3, bounded by its 0.1 % centroid
nudge), with the v^n-only foot as the failing control.
Underworld development team with AI support from Claude Code
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
…only trace) For controls and reproduction of earlier runs. Verified a strict no-op for a steady velocity (4e-17) and, on Blankenbach 1a, bit-for-bit reproduction of the published nodal SLCN run when off. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
…fn is its symbol The solvers pass V_fn as v.sym, so the previous velocity was evaluated at nodes nudged 0.1 % toward their cell centroids, a bias of 0.001 h |grad v| that the mid-time extrapolation fed into every trace. On Blankenbach 1a it moved the wall Nusselt number by 0.9 % (a 5e-4 first-cell temperature change; interior transport and Vrms unchanged), and restarts from either state relaxed to distinct wall values within 50 steps. Resolving the symbol to its mesh variable (meshVariable_lookup_by_symbol) and copying restores the exact no-op for a steady velocity (4e-17) and the published nodal wall value. Diagnostics that cleared the alternatives: recording without using the extrapolation, and adding an unrelated field after the first solve, both left the wall value unchanged. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
V_fn is symbolic by design (-v, v/2, v - v_mesh must just work), so the
previous velocity is no longer a separate field evaluated from it, nor is
V_fn resolved to a mesh variable. Both schemes now find the mesh variables
V_fn contains, keep a copy of each per history level, and form v^{n-1} as
V_fn with those variables substituted by their copies: exact for any
expression, an analytic V_fn reduces to itself. Shared helpers on _DDtBase
(_make_velocity_level, _copy_velocity_level). The steady no-op holds for
-v/2 (2e-19); tests cover V_fn as the variable, -v and v/2 for both schemes.
Underworld development team with AI support from Claude Code
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
Substituting snapshots of the mesh variables into V_fn misses everything else the expression depends on: a constant that ramps, a swarm proxy, a mesh that has moved. The previous velocity is now V_fn evaluated at the true node coordinates of a vector field (highest degree of the variables in V_fn, 2 for an analytic velocity) and cached per history level, in both schemes. No nudge: the evaluator is exact at node coordinates on simplex, quad and annulus meshes (2e-16 for an expression of a variable, a constant and the coordinates), which is what the earlier nudged evaluation lacked. Tests: V_fn as the variable, -v, v/2, and c*v with c changed between the two steps (the case substitution gets wrong), for both schemes; steady no-op 4e-16. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
Two-rank run had one rank fail and the other block in the next collective. The evaluate test now keeps only the points the rank owns and compares to round-off (the evaluator adds an ulp on the way out); the rotating-Gaussian peak is a global maximum; the hand-quadrature test is marked serial-only. Two ranks: 33 passed, 1 skipped; serial unchanged. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
Adversarial reviewWe went looking for the ways this can be wrong before it is merged. Findings, with what we did about each.
Underworld development team with AI support from Claude Code |
There was a problem hiding this comment.
🟡 Changes recommended
There are correctness/robustness issues to address (notably a mutable default argument in the new IntegrationPointSemiLagrangian API and a misleading configuration error message).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces integration-point (quadrature-point) stored fields as a first-class discretisation primitive, and builds a new semi-Lagrangian time-history scheme that samples and stores the advected history directly at those integration points (reducing the “interpolant of an interpolant” error path in the weak form). It also upgrades both semi-Lagrangian schemes’ RK2 trace-back to use a mid-time velocity extrapolation for second-order accuracy in unsteady flows.
Changes:
- Add
uw.discretisation.IntegrationPointVariablebacked by a new PETSc “delta space” quadrature-point element, including evaluation semantics and guards. - Add
uw.systems.ddt.IntegrationPointSemiLagrangianand introduce mid-time velocity extrapolation support in the semi-Lagrangian trace-back. - Add tests and developer documentation covering the new element, variable semantics, guards, and semi-Lagrangian behavior.
File summaries
| File | Description |
|---|---|
| tests/test_0064_quadrature_point_fe.py | Validates the quadrature-point (“delta”) element tabulation and dof layout. |
| tests/test_0065_integration_point_variable.py | Tests IntegrationPointVariable layout, assembler read semantics, evaluate semantics, and guard rails. |
| tests/test_0066_integration_point_slcn.py | Tests integration-point SLCN departure-point sampling accuracy, oversampling refusal, and mid-time velocity behavior. |
| src/underworld3/utilities/_jitextension.py | Refuses derivatives of integration-point variable symbols at JIT/codegen time. |
| src/underworld3/systems/ddt.py | Adds mid-time velocity snapshot logic and new IntegrationPointSemiLagrangian implementation. |
| src/underworld3/systems/init.py | Exposes the new DDt class through the systems API. |
| src/underworld3/function/_function.pyx | Implements IntegrationPointVariable evaluation as “nearest integration point in owning cell” during interpolation. |
| src/underworld3/discretisation/enhanced_variables.py | Adds public IntegrationPointVariable wrapper and base-class delegation hook. |
| src/underworld3/discretisation/discretisation_mesh.py | Introduces mesh.integration_rule and solver-side verification against the mesh’s integration rule. |
| src/underworld3/discretisation/discretisation_mesh_variables.py | Adds _BaseIntegrationPointVariable implementing quadrature-point layout and nearest-point evaluation. |
| src/underworld3/discretisation/init.py | Exports IntegrationPointVariable from the discretisation package. |
| src/underworld3/cython/uw_delta_space.h | Implements and registers the uwdelta PetscSpace plugin (header-only). |
| src/underworld3/cython/petsc_quadrature_fe.pyx | Creates the delta FE and provides utilities (tabulation + cell quadrature point coordinates). |
| src/underworld3/cython/petsc_generic_snes_solvers.pyx | Verifies solver quadrature matches mesh rule when integration-point variables are present. |
| setup.py | Adds the new Cython extension build for petsc_quadrature_fe. |
| docs/developer/subsystems/integration-point-variables.md | Adds subsystem documentation describing semantics, guards, and the integration-point SLCN. |
| docs/developer/index.md | Links the new subsystem documentation into the developer docs toctree. |
Review details
- Files reviewed: 17/17 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| continuous: bool = True, | ||
| varsymbol: Optional[str] = None, | ||
| verbose: bool = False, | ||
| bcs=[], | ||
| order: int = 1, | ||
| theta: float = 0.5, | ||
| monotone_mode: Optional[str] = None, | ||
| **_unsupported, | ||
| ): | ||
| super().__init__() | ||
| if vtype != VarType.SCALAR: | ||
| raise NotImplementedError( | ||
| "IntegrationPointSemiLagrangian: scalar histories only for now" | ||
| ) | ||
| self.monotone_mode = monotone_mode | ||
| self.mesh = mesh | ||
| self.bcs = bcs | ||
| self.verbose = verbose | ||
| self.degree = degree | ||
| self.continuous = continuous | ||
| self.order = order | ||
| self.theta = float(theta) | ||
| self.V_fn = V_fn |
There was a problem hiding this comment.
Fixed: bcs=None with a fresh list per instance (the pre-existing SemiLagrangian has the same bcs=[] default; left as is here, outside this PR's scope).
| f"IntegrationPointSemiLagrangian: the mesh rule has {Nq} points per cell " | ||
| f"but a degree-{degree} history has {local_dofs} local dofs; the " | ||
| "least-squares fit is not oversampled and is unstable at small Courant " | ||
| f"number. Build the mesh with qdegree >= {self.mesh.qdegree + 1}." | ||
| ) |
There was a problem hiding this comment.
Fixed: the message now scans PETSc's default rules for this cell type and reports the smallest qdegree with more points than the history's local dofs and the one reaching 2x oversampling (P2 on triangles: 3 and 3; P3: 3 and 5).
…rame CI (test_1056) caught the units-active SLCN diverging 63 % from the non-dimensional run: the velocity cache was created without units and filled with dimensional values from evaluate, while .data is the non-dimensional store, so the mid-time expression mixed frames. The cache variable now carries V_fn's units and every evaluated value is reduced with _to_nondim_ndarray before storing (the nodal history's own idiom, issue #267); the same for the integration-point history's snapshots and slots and its trace velocities. test_1056 now covers both schemes. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
|
CI caught a units-frame bug in the new velocity cache (test_1056: units-active SLCN 63 % off the non-dimensional run). The cache variable was created without units and filled with dimensional values from Underworld development team with AI support from Claude Code |
…ure/quadrature-point-space Conflict in _jitextension.py ccode_patch_fns: keep both the integration-point derivative guard and the component_offsets from #688. uw.systems.AdvDiffusion now names the composed Eulerian solver, so the SLCN tests and docs use AdvDiffusionSLCN explicitly. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
The composed AdvDiffusion (#688) applies its theta-rule diffusive flux to the history level, which differentiates the slot; the JIT guard refuses a gradient of a delta field. The SLCN solver's separate nodal DFDt is the structure this history needs. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
|
Merged We tried the integration-point history as the composed solver's transport manager. The base-class manager interface accepts it, but the composed solver's theta rule applies the diffusive flux to the history level, which differentiates the slot, and the JIT guard refused (a delta field has no gradient; silently assembling zero would have been the failure mode). So the integration-point history remains an Underworld development team with AI support from Claude Code |
…DF2 and theta = 1 The composed solver (#688) takes the history as its transport manager. At order 2 and at theta = 1 no spatial term sits on the old level, so the integration-point history runs there and matches the SLCN solver's field to 3e-3 on a rotating Gaussian. theta = 1 needed one change: the old-level Adams-Moulton weight is identically zero there but was a runtime constant, so 0 * grad(psi*) was still differentiated at code generation and the guard fired on a dead term; spatial_weights now returns a literal zero for theta = 1 (for every manager). The Crank-Nicolson flux differentiates the old level and stays with AdvDiffusionSLCN; the guard's refusal is tested. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
|
Reachability in the merged default solver, measured. The composed Underworld development team with AI support from Claude Code |
…g message computes the needed qdegree Copilot review on #703: bcs=[] was a shared mutable default (now None -> a fresh list per instance), and the under-sampling error suggested qdegree + 1 regardless of the history degree. The message now scans PETSc's default rules for this cell type and reports the smallest qdegree with more points than local dofs and the one reaching 2x (P2 on triangles: 3 and 3; P3: 3 and 5). Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
There was a problem hiding this comment.
🔵 Needs a closer look
It introduces new PETSc/Cython discretisation infrastructure and modifies core evaluation/assembly paths, which warrants careful human review beyond automated checks.
Review details
- Files reviewed: 18/18 changed files
- Comments generated: 3
- Review effort level: Lite
| kwargs.pop("degree", None) | ||
| kwargs.pop("continuous", None) |
| **_unsupported, | ||
| ): | ||
| super().__init__() | ||
| if vtype != VarType.SCALAR: |
| What is not here (yet): vector/tensor histories, units-aware velocity | ||
| reduction, ALE / old-frame trace-back, forcing history, checkpoint state. | ||
| Use :class:`SemiLagrangian` for those. |
|
Measurements on this branch, prompted by two questions: does the growth get The answers are no and no. The risk is at LOW Courant, and the required The growth is a low-Courant phenomenonRigid rotation, P2,
Energy is gained at Courant 0.25 and lost at Courant 2. Over 600 steps at This matters because the Courant number is chosen in the mean: in a real run It is not a sub-grid null spaceThe cell-local sampling matrix
So the extra points are not helping by improving cell-local conditioning, and The required damping has a closed formA grid-scale mode decays per step like The velocity cancels. Tested by sweeping cell Péclet at two Courant numbers,
Pe* falls 2467 -> 987 as C falls 0.25 -> 0.1 and the measured crossover moves Earlier, at Courant 0.25 over 300 steps, cell Pe 1000 was already enough to SuggestionReplace the hard refusal at 2x with a computed stabilisation:
Three properties make this behave: it is local, so slow cells are stabilised One honest difference from SUPG, and the reason for reporting rather than Not established
Scripts are in this session's scratch; happy to fold them into Underworld development team with AI support from Claude Code |
|
Reviewing the growth-factor analysis above rather than the PR as a whole. It is 1. Conditioning rules out one explanation without supplying anotherThe table shows But the conclusion drawn from it, that this is therefore an inter-cell effect, The explanation that fits both facts is sampling density rather than 2.
|
|
Follow-up, after Louis exercised the scheme: the stabilising term should ship The recommendation is to keep it as a documented option, inactive for all but Three reasons it should not be on by default:
Suggested shape, for whoever picks this up: ddt = uw.systems.ddt.IntegrationPointSemiLagrangian(
mesh, T, V, degree=2, order=1,
stabilise=False, # default; True adds kappa_stab per cell
)
The derivation, the measured constants and the caveats are in the comment Worth documenting even though it is off: the reason to write it down is that Underworld development team with AI support from Claude Code |
… _workVar (underworldcode#704) Both parameters were accepted, stored and used only to size a work variable that nothing read: the trace-back samples at psi_star's own nodes and every projection overwrote the work variable's symbol before solving. Sweeping them left the answer bit-identical while the variable spanned 98 to 972 nodes (issue underworldcode#704). Drop the pair, the allocation, its remesh registration and the docstring/comment that claimed they set the sample points; the projection solver's placeholder source is psi_fn. Denser sampling at the integration points is a separate history manager (PR underworldcode#703). Fixes underworldcode#704. Underworld development team with AI support from Claude Code Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G
Summary
A field stored at the mesh integration points, and a semi-Lagrangian history built on it.
uw.discretisation.IntegrationPointVariableis a peer ofMeshVariableand swarm variables: one value per quadrature point per cell, on an element whose basis is the identity on the mesh rule (mesh.integration_rule). The assembler reads its values at the integration points with no interpolation, so it carries values that are injected into the weak form: a semi-Lagrangian history sampled at the departure points of the integration points, or a material property reconstructed from a swarm with sub-cell resolution.PetscSpacetype (uwdelta,PetscSpaceRegister), so it needs no PETSc patch and works on stock conda PETSc. PETSc's ownPETSCSPACEPOINTcannot be tabulated anywhere but its own points in its own order, which breaksPetscFESetUp, face tabulation and boundary integrals.evaluate()is defined as the nearest integration point of the owning cell, the one extension under which a query agrees with what the assembler used at that point. Exact at the variable's own coordinates.uw.systems.ddt.IntegrationPointSemiLagrangiandrops intoAdvDiffusionasDuDt. Its slots are integration-point variables; the value the weak form sees at each integration point is the solution fromk+1steps ago evaluated exactly at that point's departure point. A delta field cannot be sampled off its points, so the slot-to-slot chain is replaced by nodal snapshots of the solution and cached velocities at the lastordertimes, with slotkfilled by tracingk+1RK2 segments back from every integration point.Both semi-Lagrangian schemes now take the RK2 mid-point velocity at the mid time,
1.5 v^n - 0.5 v^{n-1}; withv^nalone the trace is first order in an unsteady flow. The previous velocity isV_fnevaluated at the true nodes and cached per history level, so any expression (-v,v/2,c(t) v,v - v_mesh) is carried as it was at that time. On by default (midtime_velocity=Falsereproduces the earlier trace, for controls).Measurements
Rotating Gaussian, P2,
cellSize=0.05, half a revolution, L2 error, nodal SLCN vs integration-point (12 points per cell):Energy (∫T²) over 63 steps at Courant 0.5: nodal −1.4 %, integration-point −0.03 %.
Blankenbach 1a (box, Ra 1e4, P2,
cellSize=1/32, Courant 1, t = 0.3): interior transport and Vrms identical across the schemes to four digits; wall Nusselt numbers within the first-cell band (a 5e-4 temperature difference in the wall cells). Not a measured gain at cell Péclet of order one, as expected.The rule must oversample the history space
The Galerkin step with a sampled load is algebraically the weighted least-squares fit of the samples on the rule's points; the shifted field is piecewise polynomial on the shifted mesh and has sub-cell kinks, and the fit contracts only in the sampled norm. One-step growth factors by power iteration at Courant 0.25 (P2 on triangles): 6 points per cell (
qdegree=2) 1.028, 9 points (conical rule) 1.005, 12 points (qdegree=3) 1.0003; with diffusion at cell Péclet 100: 1.0095, 0.996, 0.996. Nodal SLCN: 0.9989. So the scheme does not dissipate and relies on oversampling or physics; the constructor raises when the rule has no more points per cell than the history space has local dofs and warns below 2x. P1 histories are 2x oversampled atqdegree=2; P2 needsqdegree=3(or the conical rule). Raising the rule costs every solver on the mesh its assembly time.Not in this PR
Vector/tensor histories (the viscoelastic stress is the intended next case), ALE and old-frame trace-back for the integration-point scheme, checkpoint state for it, the conical rule as a mesh option, a swarm-to-integration-point reconstruction.
Tests
tests/test_0064_quadrature_point_fe.py(element: identity tabulation on triangle/tet/quad/hex, cell-only layout, zeros off-rule),tests/test_0065_integration_point_variable.py(assembler reads the stored values: integral equals the hand quadrature sum with a one-point perturbation control; P2 projection exact;evaluateexact at own points and nearest-in-cell elsewhere; both guards),tests/test_0066_integration_point_slcn.py(exact departure-point values for one and two segments; under-sampled rule refused; rotating Gaussian beats nodal; mid-time velocity exact forv,-v,v/2,c(t) vwith thev^n-only foot as control). Full suite: 1695 passed. Docs:docs/developer/subsystems/integration-point-variables.md.Underworld development team with AI support from Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01MSGAFeA7qYXgkuw9ud8F2G