Note
Document Purpose This guide documents the established patterns, conventions, and architectural decisions for Underworld3 development. It serves as a detailed reference for maintaining consistency across the codebase.
The normative style contract is the UW3 Style Charter — where this guide and the Charter disagree, the Charter wins. See the authority map in the developer documentation index.
Code Organization¶
Directory Structure¶
Source code:
underworld3/src/underworld3/Documentation:
underworld3/docs/Tests:
underworld3/tests/Utilities:
underworld3/src/underworld3/utilities/
Import Patterns¶
# Utilities are imported and made available
from underworld3.utilities import NDArray_With_Callback
# MPI access pattern
import underworld3 as uw
if hasattr(uw, 'mpi') and hasattr(uw.mpi, 'barrier'):
uw.mpi.barrier()
# Synchronised updates pattern
import underworld3 as uw
with uw.synchronised_array_update():
# Batch operations here
pass
Naming Conventions¶
Private attributes: Use
_prefix (e.g.,_particle_coordinates,_clip_to_mesh)Internal methods: Use
_prefix (e.g.,_trigger_callback,_on_data_changed)Public properties: No prefix, use descriptive names (e.g.,
data,clip_to_mesh)Context managers: Use descriptive names (e.g.,
delay_callback,dont_clip_to_mesh)
Property Patterns¶
Reactive Data Properties¶
Properties should return array-like objects that can trigger updates when modified:
class Field:
@property
def data(self):
"""Field data with reactive callbacks."""
if self._cached_data is None:
self._cached_data = NDArray_With_Callback(
self._values,
owner=self
)
self._cached_data.set_callback(self._on_values_changed)
return self._cached_data
def _on_values_changed(self, array, change_info):
# Invalidate cached computations that depend on the values
self._interpolant = None
self._stats_cache = None
Property with Getter/Setter Pattern¶
@property
def clip_to_mesh(self):
return self._clip_to_mesh
@clip_to_mesh.setter
def clip_to_mesh(self, value):
self._clip_to_mesh = bool(value)
Array-like Property Access¶
When properties need to behave like arrays but with additional functionality:
# Users index into the property: var.data[...] rather than rebinding var.data
# Properties return NDArray_With_Callback for transparent numpy compatibility
Documentation Style¶
NumPy/Sphinx Docstrings¶
Docstrings use NumPy style with RST markup (:math:, double backticks
for code). This is the settled standard (Style Charter §6); the docstrings turn
into the Sphinx documentation and render well in Jupyter. The conversion of older
docstrings is tracked in docs/plans/docstring-conversion-plan.md.
def solve_diffusion(kappa, delta_t, monotone=False):
r"""Advance the diffusion equation by one timestep.
Integrates :math:`\partial_t T = \nabla \cdot (\kappa \nabla T)` with an
implicit theta-scheme.
Parameters
----------
kappa : float or sympy expression
Diffusivity :math:`\kappa`. May depend on position or on other
mesh variables.
delta_t : float
Timestep. Non-dimensionalised internally when the model carries units.
monotone : bool, default=False
Clamp interpolation overshoot during the semi-Lagrangian trace-back.
Returns
-------
MeshVariable
The updated temperature field.
Examples
--------
>>> T_new = solve_diffusion(1.0, 0.01)
Notes
-----
We do not shy away from equations in docstrings: state the weak form or the
scheme where it aids the reader.
"""
Key Documentation Elements¶
NumPy sections in the standard order: summary line, extended description,
Parameters,Returns,Examples,Notes,See AlsoMathematical notation via RST
:math:roles (raw stringsr"""for backslashes)Provide complete, runnable examples
Include performance considerations where they matter (in
Notes)
Array and Data Management¶
NDArray_With_Callback Pattern¶
For reactive array data that needs to trigger updates:
# Constructor pattern (array data first, like numpy)
arr = NDArray_With_Callback([1, 2, 3]) # Basic usage
arr = NDArray_With_Callback(data, owner=self) # With ownership
arr = NDArray_With_Callback(data, owner=self, callback=func) # With callback
# Callback signature
def callback(array: NDArray_With_Callback, change_info: dict) -> None:
# change_info contains: operation, indices, old_value (always None,
# retained for compatibility), new_value, array_shape, array_dtype
pass
# For callbacks that synchronise a variable's CANONICAL storage, register
# with add_canonical_callback(func): it guards against firing on derived
# views/copies (rank-asymmetric collectives, #376) and hands the callback
# the canonical array every time.
Array vs Data Property Shapes¶
# Array property: (N, a, b) format - PREFERRED
scalar.array.shape # (N, 1, 1)
vector.array.shape # (N, 1, dim)
tensor.array.shape # (N, dim, dim)
# Data property: (-1, components) format - BACKWARD COMPATIBILITY
scalar.data.shape # (N, 1)
vector.data.shape # (N, dim)
tensor.data.shape # (N, 6) for symmetric
# Indexing patterns
scalar.array[:, 0, 0] = values # Scalar assignment
vector.array[:, 0, i] = component_i # Vector component
vector.array[:, 0, :] = all_components # Full vector
Data Access Patterns¶
# Preferred: Direct array access with proper indexing
temperature.array[:, 0, 0] = temp_values # Scalar
velocity.array[:, 0, :] = vel_field # Vector
# Mesh coordinates: read via mesh.X.coords; deform via mesh.deform()
coords = mesh.X.coords # (N, dim) read access
mesh.deform(mesh.X.coords + displacement) # coordinate changes go through deform()
# Swarm particle positions: the public coords property (getter and setter)
swarm.coords = swarm.coords + displacement
# Avoid: Incorrect indexing
# scalar.array[:, 0] = values # Missing third index!
# vector.array[:, i] = values # Missing middle index!
# Avoid: deprecated coordinate accessors (kept only so old code runs)
# mesh.data, mesh.points, swarm.data, swarm.points
Context Managers¶
Direct Array Access Pattern (Preferred)¶
For most operations, use direct array access without context managers:
# Single variable - no context needed
temperature.array[:, 0, 0] = initial_values
velocity.array[:, 0, :] = velocity_field
# Multiple variables - use synchronised update
with uw.synchronised_array_update():
temperature.array[:, 0, 0] = temp_values
velocity.array[:, 0, :] = vel_values
pressure.array[:, 0, 0] = press_values
# All arrays synchronized here
Legacy Access Context (Deprecated)¶
The old pattern still works but is no longer recommended:
# OLD - Still works but deprecated
with mesh.access(var):
var.data[...] = values
Delay Callback Pattern¶
For batching operations and MPI synchronization:
# Single array
with arr.delay_callback("batch update"):
arr[0] = 1
arr[1] = 2
arr[2] = 3
# All callbacks fire here with MPI barriers
# Global coordination across several variables
with NDArray_With_Callback.delay_callbacks_global("field update"):
temperature.array[:, 0, 0] = temp_values
velocity.array[:, 0, :] = vel_values
# Synchronized execution across all arrays
# (uw.synchronised_array_update() is the public spelling of this context)
Custom Context Managers¶
def dont_clip_to_mesh(self):
"""Context manager that temporarily disables mesh clipping."""
class _ClipToggleContext:
def __init__(self, swarm):
self.swarm = swarm
self.original_value = None
def __enter__(self):
self.original_value = self.swarm._clip_to_mesh
self.swarm._clip_to_mesh = False
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.swarm._clip_to_mesh = self.original_value
return _ClipToggleContext(self)
MPI and Parallel Patterns¶
MPI Integration¶
# Safe MPI import pattern
try:
import underworld3 as uw
_has_uw_mpi = hasattr(uw, 'mpi') and hasattr(uw.mpi, 'barrier')
except ImportError:
_has_uw_mpi = False
uw = None
# MPI barrier usage in context managers
if _has_uw_mpi:
try:
uw.mpi.barrier()
except Exception as e:
logger.warning(f"MPI barrier failed: {e}")
Parallel Context Synchronization¶
Entry barrier: All processes enter context together
Pre-callback barrier: All processes finish operations before callbacks
Exit barrier: All processes complete callbacks before context exit
Thread Safety¶
Use
threading.local()for thread-local storageImplement proper locking for shared resources
Use weak references to prevent circular dependencies
Callback and Event Systems¶
Callback Registration Patterns¶
# Multiple callback support
arr.set_callback(callback) # Replace existing
arr.add_callback(callback) # Add additional
arr.remove_callback(callback) # Remove specific
arr.clear_callbacks() # Remove all
# Enable/disable for performance
arr.disable_callbacks() # Batch operations
arr.enable_callbacks() # Re-enable
Error Handling in Callbacks¶
for callback in self._callbacks.copy():
try:
callback(self, change_info)
except Exception as e:
logger.warning(f"Callback error in {callback}: {e}")
# Continue with other callbacks
Owner Pattern¶
# Weak reference to owner
self._owner = weakref.ref(owner) if owner is not None else None
# Safe owner access
@property
def owner(self):
return self._owner() if self._owner is not None else None
Testing Patterns¶
Test Structure¶
def test_feature_name(setup_data):
# Arrange
obj = setup_data
obj.configure_for_test()
# Act
result = obj.perform_operation()
# Assert
assert result.meets_expectations()
np.testing.assert_allclose(expected, actual, rtol=1e-15)
Callback Testing¶
def test_callback_triggering():
execution_log = []
def test_callback(array, info):
execution_log.append(f"{info['operation']} at {info['indices']}")
arr = NDArray_With_Callback([1, 2, 3])
arr.set_callback(test_callback)
arr[0] = 99
assert len(execution_log) == 1
assert "setitem at 0" in execution_log[0]
File and Directory Conventions¶
New Utility Files¶
Location:
underworld3/src/underworld3/utilities/Import: Add to
utilities/__init__.pyPattern:
from .filename import ClassName
Documentation Files¶
Developer docs:
underworld3/docs/developer/Format: MyST Markdown (
.md), built with Sphinx — see the “Documentation Requests” section ofCLAUDE.mdfor admonition/math syntaxNaming: Descriptive names with purpose (e.g.,
UW3_Developers_NDArrays.md)Integration: add new documents to the appropriate
toctreein the parentindex.mdand verify withpixi run docs-build
Test Files¶
Location:
underworld3/tests/Naming:
test_NNNN_description.pyUse fixtures for setup/teardown
Performance Considerations¶
Callback Performance¶
Zero overhead when callbacks disabled
Minimal impact (< 5% typical) when enabled
Use delayed contexts for batch operations
Disable callbacks during bulk modifications
Memory Management¶
Use weak references for owner relationships
Clean up cached data appropriately
Avoid circular dependencies
MPI Performance¶
Batch operations within delay contexts
Minimize barrier frequency
Use appropriate synchronization points
Common Patterns Summary¶
Essential Patterns¶
Reactive Properties: Return NDArray_With_Callback with owner and callbacks
Context Managers: Use for state management and batch operations
MPI Integration: Always include barriers with error handling
Documentation: NumPy/Sphinx docstrings with RST
:math:; MyST.mddocsTesting: Comprehensive callback and functionality testing
Error Handling: Graceful degradation and logging
Performance: Provide enable/disable mechanisms for expensive operations
Migration Patterns¶
Pattern |
Legacy |
Current |
Future |
|---|---|---|---|
Array Access |
|
|
Direct access preferred |
Multi-Variable |
|
|
Batch context |
Documentation |
Plain markdown |
MyST markdown (Sphinx) |
Enhanced features |
Testing |
Ad-hoc patterns |
Structured fixtures |
Comprehensive coverage |
Quality Guidelines¶
Tip
Code Quality Checklist
Proper error handling with logging
Thread-safe operations where needed
MPI barriers for parallel coordination
Comprehensive docstrings with examples
Unit tests for new functionality
Performance considerations documented
Backward compatibility preserved
Tip
Contributing This guide should be updated as new patterns emerge and existing patterns evolve. For questions or suggestions, please see the Contributing Guidelines or open an issue on the Underworld3 repository.
Last updated: 2026-07 (Wave E docs alignment — docstring/doc-format/coordinate sections brought in line with the UW3 Style Charter)