Add February 2026 Architectural Reviews - #45
Conversation
…tems - UNITS-SYSTEM-ARCHITECTURAL-REVIEW.md: Documents the Gateway Pattern architecture, ~6,155 LOC across 6 core modules, and January 2026 consolidation that removed ~940 lines of deprecated mixin code - DATA-ACCESS-MATHEMATICAL-INTERFACE-REVIEW.md: Documents NDArray_With_Callback, MathematicalMixin, and EnhancedMeshVariable wrapper layer with delegation pattern fix that eliminated 425 lines of duplicate code - Updated docs/reviews/README.md with February 2026 section These reviews supersede the November 2025 reviews (UW3-2025-11-002 and UW3-2025-11-003) which were based on a now-outdated codebase state. Underworld development team with AI support from Claude Code
🔍 Architectural Review ChecklistThank you for submitting an architectural review! Reviewers should validate: Design & Architecture
Implementation
Testing & Validation
Documentation
Review Process: See CODE-REVIEW-PROCESS.md Approval: This PR merge = Review formally approved |
|
✅ Test Suite: success |
There was a problem hiding this comment.
Pull request overview
Adds February 2026 architectural review documentation for the Units System and the Data Access / Mathematical Interface, and updates the reviews index to reference these new documents and include them in the summary table.
Changes:
- Added two new architectural review documents under
docs/reviews/2026-02/. - Updated
docs/reviews/README.mdwith a new “2026 Reviews” section and new rows in the Review Summary Table. - Updated the README “Last Updated” date.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| docs/reviews/README.md | Adds Feb 2026 review entries and includes them in the summary table. |
| docs/reviews/2026-02/UNITS-SYSTEM-ARCHITECTURAL-REVIEW.md | New Units System architectural review document (UW3-2026-02-001). |
| docs/reviews/2026-02/DATA-ACCESS-MATHEMATICAL-INTERFACE-REVIEW.md | New Data Access / Mathematical Interface architectural review document (UW3-2026-02-002). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | `scaling/_scaling.py` | ~656 | Model scaling infrastructure | | ||
| | `scaling/units.py` | ~1,993 | Pint registry, unit definitions | | ||
| | `function/quantities.py` | ~859 | UWQuantity class | | ||
| | `function/expressions.py` | ~1,797 | UWexpression (lazy evaluation) | | ||
| | `function/nondimensional.py` | ~350 | Non-dimensionalization utilities | | ||
| | `function/unit_conversion.py` | ~500 | Conversion utilities, get_units() | |
There was a problem hiding this comment.
Several module paths in this metrics table don't exist in the repository (e.g., src/underworld3/scaling/units.py and src/underworld3/function/nondimensional.py). Consider updating these entries to the actual locations (notably src/underworld3/units.py and src/underworld3/utilities/nondimensional.py / src/underworld3/scaling/_scaling.py) so readers can navigate to the code.
| | `scaling/_scaling.py` | ~656 | Model scaling infrastructure | | |
| | `scaling/units.py` | ~1,993 | Pint registry, unit definitions | | |
| | `function/quantities.py` | ~859 | UWQuantity class | | |
| | `function/expressions.py` | ~1,797 | UWexpression (lazy evaluation) | | |
| | `function/nondimensional.py` | ~350 | Non-dimensionalization utilities | | |
| | `function/unit_conversion.py` | ~500 | Conversion utilities, get_units() | | |
| | `src/underworld3/scaling/_scaling.py` | ~656 | Model scaling infrastructure | | |
| | `src/underworld3/units.py` | ~1,993 | Pint registry, unit definitions | | |
| | `src/underworld3/function/quantities.py` | ~859 | UWQuantity class | | |
| | `src/underworld3/function/expressions.py` | ~1,797 | UWexpression (lazy evaluation) | | |
| | `src/underworld3/utilities/nondimensional.py` | ~350 | Non-dimensionalization utilities | | |
| | `src/underworld3/function/unit_conversion.py` | ~500 | Conversion utilities, get_units() | |
There was a problem hiding this comment.
Fixed — the metrics table now uses the correct paths: units.py, utilities/nondimensional.py, etc. Verified against the current source tree.
| class UWexpression: | ||
| @property | ||
| def units(self): | ||
| # Always derived, never stored separately | ||
| if self._value_with_units is not None: | ||
| return self._value_with_units.units # From contained atom | ||
| return get_units(self._sym) # From contained tree |
There was a problem hiding this comment.
The example UWexpression.units implementation here refers to _value_with_units and get_units(self._sym), but the current implementation in src/underworld3/function/expressions.py derives units via self._sym.units (and _value_with_units is not an attribute on UWexpression). Please update this snippet to match the actual code to avoid misleading readers about where unit metadata lives.
There was a problem hiding this comment.
Fixed — the code snippet now matches the actual implementation in expressions.py:881: derives units via self._sym.units.
| | `src/underworld3/scaling/_scaling.py` | Model scaling infrastructure | | ||
| | `src/underworld3/scaling/units.py` | Pint registry configuration | | ||
| | `src/underworld3/function/quantities.py` | UWQuantity implementation | | ||
| | `src/underworld3/function/expressions.py` | UWexpression implementation | | ||
| | `src/underworld3/function/nondimensional.py` | Non-dimensionalization | | ||
| | `src/underworld3/function/unit_conversion.py` | get_units(), has_units() | |
There was a problem hiding this comment.
The "Key Files" table lists src/underworld3/scaling/units.py and src/underworld3/function/nondimensional.py, which are not present, and it attributes get_units()/has_units() to function/unit_conversion.py (those entry points are in src/underworld3/units.py). Please correct these references so the document points to the real source files.
There was a problem hiding this comment.
Fixed — Key Files table corrected to match actual source paths.
| EnhancedMeshVariable (persistence.py) ← THIS IS WHAT USERS GET | ||
| - Wraps _BaseMeshVariable | ||
| - Adds: Math operations, units support, persistence | ||
| - DELEGATES .array property to base | ||
| ↓ | ||
| _BaseMeshVariable (discretisation_mesh_variables.py) | ||
| - Low-level PETSc interface | ||
| - Owns array view classes (SimpleMeshArrayView, TensorMeshArrayView) | ||
| - Direct PETSc vector management | ||
| ``` | ||
|
|
||
| **Key Discovery**: `MeshVariable` is an **alias** for `EnhancedMeshVariable`: | ||
| ```python | ||
| # src/underworld3/discretisation/__init__.py line 2: | ||
| from .persistence import EnhancedMeshVariable as MeshVariable | ||
| ``` |
There was a problem hiding this comment.
The architecture diagram + alias snippet points to EnhancedMeshVariable (persistence.py) and shows from .persistence import EnhancedMeshVariable as MeshVariable, but src/underworld3/discretisation/__init__.py currently imports EnhancedMeshVariable from .enhanced_variables (line 28). Please update the diagram and code snippet to reflect the actual module so readers can find the implementation.
There was a problem hiding this comment.
Fixed — the diagram and alias snippet now correctly reference enhanced_variables.py:
from .enhanced_variables import EnhancedMeshVariable as MeshVariable|
|
||
| ## Core Components | ||
|
|
||
| ### 1. NDArray_With_Callback (`utilities/nd_array_with_callback.py`) |
There was a problem hiding this comment.
This section references utilities/nd_array_with_callback.py, but the NDArray_With_Callback implementation is in src/underworld3/utilities/nd_array_callback.py. Updating the file name here (and anywhere else it’s cited) will keep the review navigable.
| ### 1. NDArray_With_Callback (`utilities/nd_array_with_callback.py`) | |
| ### 1. NDArray_With_Callback (`src/underworld3/utilities/nd_array_callback.py`) |
There was a problem hiding this comment.
Fixed — all references now use nd_array_callback.py (the actual filename).
| ### 3. EnhancedMeshVariable (`discretisation/persistence.py`) | ||
|
|
||
| User-facing wrapper combining multiple capabilities: | ||
|
|
||
| ```python | ||
| class EnhancedMeshVariable(UnitAwareMixin, MathematicalMixin): | ||
| """ | ||
| Enhanced MeshVariable with: | ||
| - Mathematical operations (via MathematicalMixin) | ||
| - Units support (via UnitAwareMixin) |
There was a problem hiding this comment.
The EnhancedMeshVariable example class signature uses UnitAwareMixin, but the current implementation is class EnhancedMeshVariable(DimensionalityMixin, MathematicalMixin) in src/underworld3/discretisation/enhanced_variables.py. Please align the snippet and surrounding text with the real mixin names to avoid confusion.
| ### 3. EnhancedMeshVariable (`discretisation/persistence.py`) | |
| User-facing wrapper combining multiple capabilities: | |
| ```python | |
| class EnhancedMeshVariable(UnitAwareMixin, MathematicalMixin): | |
| """ | |
| Enhanced MeshVariable with: | |
| - Mathematical operations (via MathematicalMixin) | |
| - Units support (via UnitAwareMixin) | |
| ### 3. EnhancedMeshVariable (`discretisation/enhanced_variables.py`) | |
| User-facing wrapper combining multiple capabilities: | |
| ```python | |
| class EnhancedMeshVariable(DimensionalityMixin, MathematicalMixin): | |
| """ | |
| Enhanced MeshVariable with: | |
| - Mathematical operations (via MathematicalMixin) | |
| - Dimensionality & units support (via DimensionalityMixin) |
There was a problem hiding this comment.
Fixed — class signature now shows the actual mixins: class EnhancedMeshVariable(DimensionalityMixin, MathematicalMixin).
| | `src/underworld3/utilities/nd_array_with_callback.py` | Callback array implementation | | ||
| | `src/underworld3/utilities/mathematical_mixin.py` | Mathematical notation support | | ||
| | `src/underworld3/discretisation/persistence.py` | EnhancedMeshVariable | |
There was a problem hiding this comment.
The "Key Files" table lists paths that don't exist in this repo (e.g. src/underworld3/utilities/nd_array_with_callback.py and src/underworld3/discretisation/persistence.py as the EnhancedMeshVariable implementation). Please update these to the actual files (utilities/nd_array_callback.py and discretisation/enhanced_variables.py) so links and references remain accurate.
| | `src/underworld3/utilities/nd_array_with_callback.py` | Callback array implementation | | |
| | `src/underworld3/utilities/mathematical_mixin.py` | Mathematical notation support | | |
| | `src/underworld3/discretisation/persistence.py` | EnhancedMeshVariable | | |
| | `src/underworld3/utilities/nd_array_callback.py` | Callback array implementation | | |
| | `src/underworld3/utilities/mathematical_mixin.py` | Mathematical notation support | | |
| | `src/underworld3/discretisation/enhanced_variables.py` | EnhancedMeshVariable | |
There was a problem hiding this comment.
Fixed — Key Files table corrected to nd_array_callback.py and enhanced_variables.py.
| | Review | Component | Tests | Pass Rate | Date | Status | | ||
| |--------|-----------|-------|-----------|------|--------| | ||
| | Units System (2026-02) | Units/Scaling | 60+ | 🔍 TBD | 2026-02-01 | 🔍 Under Review | | ||
| | Data Access (2026-02) | Array/Math Interface | 75+ | 🔍 TBD | 2026-02-01 | 🔍 Under Review | |
There was a problem hiding this comment.
The archive now includes two February 2026 reviews, but the later "Statistics" section still only lists year 2025 (and omits 2026 components). Please update the "By Year" / "By Component" tables so they remain consistent with the newly added 2026 entries above.
There was a problem hiding this comment.
Fixed — the Statistics section now includes 2026 entries in both "By Year" and "By Component" tables.
Add required sections to both architectural reviews: - ## Changes Made section after Overview - ## Testing Instructions (renamed from Testing Status) - ## Known Limitations as top-level section Restructure subsections to maintain proper hierarchy. Underworld development team with AI support from Claude Code
|
✅ Test Suite: success |
Address Copilot review suggestions on PR #45: - units.py is at src/underworld3/units.py (not scaling/units.py) - nondimensional.py is in utilities/ (not function/) - nd_array_callback.py (not nd_array_with_callback.py) - EnhancedMeshVariable is in enhanced_variables.py (not persistence.py) - EnhancedMeshVariable uses DimensionalityMixin (not UnitAwareMixin) - UWexpression.units implementation uses hasattr(self._sym, 'units') - Update README statistics to include 2026 reviews Underworld development team with AI support from Claude Code
|
✅ Test Suite: success |
|
Just been through to fix up the inconsistencies that copilot found. The review is our formal oversight of meeting a project milestone @jcgraciosa, @bknight1 - happy to talk you through the changes. |
There was a problem hiding this comment.
Comments and clarifications:
-
Maybe this will be for future work, but when saving data, will these be in terms of dimensional values as well?
-
To clarify, in indexing a tensor field, is it (N, row, col)?
-
For the data access patterns, is it recommended to do batch synchronization if I'm updating more than 1 variable?
-
Is the figure for the Callback Overhead (5% - 10%) also the same for parallel runs?
There was a problem hiding this comment.
Thanks for the thorough review. Responses to each question:
1. Saving dimensional data: Currently, HDF5 save/load writes non-dimensional values (what PETSc sees). Adding unit metadata to HDF5 output is on the medium-term roadmap — the plan is to store units as HDF5 attributes alongside the data arrays, so saved files can be re-loaded with full dimensional context. For now, users need to track their scaling model to re-dimensionalise on load.
2. Tensor indexing: Yes, exactly — (N, row, col). For a 3D stress tensor, stress.array[:, i, j] gives the (i,j) component across all N nodes. The convention matches standard matrix notation.
3. Batch synchronization: Yes, uw.synchronised_array_update() is recommended when updating multiple variables. Each individual write triggers a PETSc sync + MPI barriers, so batching avoids redundant synchronization. For a single variable, direct access is fine.
4. Callback overhead in parallel: The 5-10% figure is for serial. In parallel, the overhead is dominated by MPI barriers (entry, pre-callback, exit) rather than the callback mechanism itself. For large arrays the barrier cost is relatively small; for small arrays or frequent updates, batching becomes more important. We haven't profiled this systematically across different process counts yet — that's noted as a medium-term task.
Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
A few questions and clarifications:
- Do the MeshVariable data access patterns also follow the template? I.e.,
var.array[:, 0, 0] = scalar_values # should these be dimensional values?
var.data[...] = values # should these be non-dimensional values?
-
Is there an example for accessing the units of MeshVar.sym * MeshVar.sym?
-
For complex expressions, is there a recommended maximum depth for nested expressions?
There was a problem hiding this comment.
Thanks for the questions. Responses:
1. .array vs .data dimensional convention: Yes, that's the intended pattern:
var.arrayreturns values in the user's dimensional units (what you'd measure physically)var.datareturns non-dimensional values (what PETSc works with internally)
So var.array[:, 0, 0] = temperature_in_kelvin and var.data[...] = nd_values is correct. The non-dimensionalisation happens automatically based on the active scaling model.
2. Units of MeshVar.sym * MeshVar.sym: The result is a SymPy expression, and units are discoverable via get_units():
T = uw.discretisation.MeshVariable("T", mesh, 1, units="K")
v = uw.discretisation.MeshVariable("v", mesh, mesh.dim, units="m/s")
# Symbolic product
expr = T.sym * v.sym[0]
# Discover units from the expression tree
from underworld3.function.unit_conversion import get_units
units = get_units(expr) # walks the SymPy tree, finds units on atomsThe get_units() function traverses the expression tree, finds UnderworldFunction atoms, looks up their units, and combines them according to the arithmetic operations in the tree.
3. Maximum depth for nested expressions: There is no hard limit, but practically, deeply nested expressions (>10 levels of composition) can make get_units() traversal slower and unit propagation less reliable through certain SymPy transformations (e.g., simplify(), expand()). For typical geodynamics expressions (Rayleigh number, viscosity laws, yield criteria), nesting depth is 3-5 levels and works reliably. If you hit issues with very complex expressions, breaking them into named intermediate uw.expression() objects is the recommended approach — it also improves readability.
Underworld development team with AI support from Claude Code
Update sign-off tables, review comments, and README status for both the Units System and Data Access reviews following PR #45 merge. Copilot path corrections and jcgraciosa's clarification questions are documented in each review's Resolution section. Underworld development team with AI support from Claude Code
Summary
New Reviews
1. UNITS-SYSTEM-ARCHITECTURAL-REVIEW.md (UW3-2026-02-001)
Documents the current units system architecture:
2. DATA-ACCESS-MATHEMATICAL-INTERFACE-REVIEW.md (UW3-2026-02-002)
Documents the data access and mathematical interface:
velocity * densitywithout.sym)Test plan
🤖 Generated with Claude Code