Route constant solver parameters through PetscDS constants[] array#87
Route constant solver parameters through PetscDS constants[] array#87lmoresi merged 4 commits intodevelopmentfrom
Conversation
…lation The membership check "Null_Boundary" in self.natural_bcs always returned False because natural_bcs is a list of namedtuples, not strings. This caused Null_Boundary to be re-added on every _build() call, resetting is_setup and triggering full JIT recompilation on every solve() for any solver with natural boundary conditions. Fix: check bc.boundary attribute instead of string membership. Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
Constant solver parameters (viscosity, diffusivity, source terms) are now routed through PETSc's constants[] mechanism instead of being baked as C literals. Changing a constant value between solves no longer triggers JIT recompilation — only PetscDSSetConstants() is called. Key changes: - _JITConstant symbol class renders as constants[i] in generated C code - _extract_constants() identifies constant UWexpressions in expression trees - Two-phase unwrap: substitute constants with placeholders, then expand rest - Cache hash uses structural form (value-independent for constants) - PetscDSSetConstants binding added, called at setup and before each solve - Solver _setup_pointwise_functions no longer pre-unwraps expressions, preserving constant symbols for the constants extraction phase Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
The _build() method checked "Null_Boundary" in self.natural_bcs which always returned False (list of namedtuples, not strings). This caused Null_Boundary to be re-added every solve, resetting is_setup and forcing full JIT recompilation — defeating the constants[] mechanism for any solver with natural BCs. Fix: use any(bc.boundary == "Null_Boundary" for bc in self.natural_bcs). New tests verify constants work in essential BCs (Poisson with expression amplitude) and natural BCs (Stokes with expression traction), including no-recompile assertions. Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
- Rewrite expressions-functions.md: replace placeholder with comprehensive guide covering constants mechanism, two-phase unwrap, performance benefits, and integration with constitutive models - Update MathematicalObjects JIT section: document two-phase unwrap and why UWexpressions are preferred for time-varying solver parameters - Update HOW-TO-WRITE scripts guide: replace JIT debugging section with guidance on using expressions for efficient time-stepping - Add "Using Parameters with Solvers" section to beginner/parameters.md: show how to wrap Params in uw.expression() for solver use Underworld development team with AI support from Claude Code (https://claude.com/claude-code)
There was a problem hiding this comment.
Pull request overview
This PR updates Underworld3’s JIT compilation pipeline to route spatially-constant UWexpression parameters through PETSc’s constants[] array (via PetscDSSetConstants()), avoiding expensive recompilation when only parameter values change between solves.
Changes:
- Add constant extraction/substitution so constant
UWexpressionatoms compile asconstants[i](structural hashing becomes value-independent). - Update solvers to set/update PETSc DS constants before each solve (including MG coarse levels) and fix
Null_Boundaryduplication in natural BC setup. - Add functional tests and expand developer/user docs explaining the new constants mechanism and performance implications.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_1001_poisson_constants.py |
New functional tests covering Poisson/Stokes constants, no-recompile behavior, and the Null_Boundary fix. |
tests/test_0004_pointwise_fns.py |
Update call sites for new getext() return type. |
src/underworld3/utilities/_jitextension.py |
Implement _JITConstant, constant extraction/packing, structural hashing, and return constants_manifest from getext(). |
src/underworld3/cython/petsc_maths.pyx |
Update getext() usage to handle namedtuple return. |
src/underworld3/cython/petsc_generic_snes_solvers.pyx |
Add constants propagation via PetscDSSetConstants(), call updates before solve, and fix Null_Boundary membership check. |
src/underworld3/cython/petsc_extras.pxi |
Declare PetscDSSetConstants() for Cython use. |
docs/developer/subsystems/expressions-functions.md |
Document expressions + the constants mechanism and how it affects JIT performance. |
docs/developer/guides/HOW-TO-WRITE-UW3-SCRIPTS.md |
Add troubleshooting guidance for slow recompiling time-stepping loops and constant unwrapping symptoms. |
docs/developer/UW3_Developers_MathematicalObjects.md |
Document two-phase unwrap and constants-based updates without recompilation. |
docs/beginner/parameters.md |
Beginner-facing guidance to wrap solver parameters with uw.expression() for efficient updates. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from underworld3.function.expressions import ( | ||
| is_constant_expr, | ||
| extract_expressions, | ||
| UWexpression, | ||
| ) |
There was a problem hiding this comment.
_extract_constants() imports is_constant_expr and extract_expressions but neither is used, and _collect_constant_atoms() takes is_constant_expr as an argument without using it. This adds dead code/indirection and makes it harder to understand the constant detection logic. Consider removing the unused imports and the unused parameter from _collect_constant_atoms() (and its call sites).
| # Sort by name for deterministic MPI-consistent ordering | ||
| sorted_constants = sorted(constant_exprs, key=lambda e: str(e)) | ||
|
|
||
| manifest = [] | ||
| subs_map = {} | ||
| for i, expr in enumerate(sorted_constants): | ||
| jit_const = _JITConstant(i, name=f"_jit_const_{str(expr)}") |
There was a problem hiding this comment.
Constants are sorted and named using str(expr), but UWexpression.__str__ is based on the mutable display name (see UWexpression.rename()), not the symbol identity. This can make constants ordering (and thus constants[i] mapping + cache hash) change due to display-only renames, leading to unnecessary recompiles or mismatched packing. Prefer using the stable expr.name (and/or _uw_id where applicable) for deterministic ordering and placeholder naming.
| # Sort by name for deterministic MPI-consistent ordering | |
| sorted_constants = sorted(constant_exprs, key=lambda e: str(e)) | |
| manifest = [] | |
| subs_map = {} | |
| for i, expr in enumerate(sorted_constants): | |
| jit_const = _JITConstant(i, name=f"_jit_const_{str(expr)}") | |
| # Sort by a stable identifier (prefer UWexpression.name) for deterministic MPI-consistent ordering | |
| sorted_constants = sorted(constant_exprs, key=lambda e: getattr(e, "name", str(e))) | |
| manifest = [] | |
| subs_map = {} | |
| for i, expr in enumerate(sorted_constants): | |
| base_name = getattr(expr, "name", str(expr)) | |
| jit_const = _JITConstant(i, name=f"_jit_const_{base_name}") |
| except (TypeError, ValueError): | ||
| # Fallback: try .data property | ||
| try: | ||
| values[idx] = float(uw_expr.data) | ||
| except Exception: | ||
| values[idx] = 0.0 |
There was a problem hiding this comment.
_pack_constants() silently falls back to 0.0 if a constant cannot be converted to float, which can mask real configuration/errors and produce incorrect physics with no signal. It would be safer to raise a clear exception (or at least warn and include the expression name/value) so callers know constants could not be packed.
| except (TypeError, ValueError): | |
| # Fallback: try .data property | |
| try: | |
| values[idx] = float(uw_expr.data) | |
| except Exception: | |
| values[idx] = 0.0 | |
| except (TypeError, ValueError) as exc: | |
| # Fallback: try .data property | |
| try: | |
| values[idx] = float(getattr(uw_expr, "data")) | |
| except Exception as data_exc: | |
| raise RuntimeError( | |
| "Failed to pack JIT constant at index " | |
| f"{idx}: could not convert UWexpression {uw_expr!r} to float. " | |
| "Both unwrap_expression(..., mode='nondimensional') and the " | |
| ".data attribute conversion failed." | |
| ) from data_exc |
Summary
UWexpressionsolver parameters (viscosity, diffusivity, dt_elastic, BC amplitudes) are now automatically routed through PETSc'sconstants[]array instead of being baked as C literalsPetscDSSetConstants()is called (~0ms vs ~10s)_build()re-addedNull_Boundaryevery call (string membership check on list of namedtuples), forcing unnecessary recompilation for any solver with natural BCsHow it works
_extract_constants()scans expression trees forUWexpressionatoms that resolve to pure numbers_JITConstantsymbols that render asconstants[i]in generated C code_update_constants()re-packs values and callsPetscDSSetConstants()before everysnes.solve()Test plan
Underworld development team with AI support from Claude Code