UW3 Style Charter¶
Status: Normative. Every development session — human or AI — loads this document and follows it. It is deliberately short; there is no excuse for not reading all of it.
1. The Founding Rule¶
Anyone — specifically, any working geodynamicist — must be able to read this code and understand it. Over-complication and inconsistent patterns are the enemies of that rule: a clever construction that saves ten lines but costs the reader a detour has made the code worse. When in doubt, write the plain version.
Don’t forget this: Underworld3 is a Python API for building mathematical models,
particularly for geoscience. The founding rule applies as much to the Python scripts
and Jupyter notebooks as it does to the code we write under the hood. We write code
that makes models easy to read, and we always hide complex or irrelevant detail at
least one level below the notebooks and scripts. Inner workings (private
_method_xxx()) are never visible to the user.
Consistent patterns: common choices are the defaults; uncommon choices are harder to find. Documentation is great — the docstrings turn into the documentation and are Jupyter-friendly. We do not shy away from equations in docstrings, and we do provide good, simple examples. We don’t overdo it, we don’t force-feed our users with emoji, and we don’t pat ourselves on the back for our cleverness. We also don’t care how complicated something was to code or debug; we just report how it works.
2. Match the Charter, Not the Surrounding Code¶
This is a load-bearing clause. A month of drift means existing code is not a
reliable pattern to copy — drifted code copied becomes drift squared. You MUST follow
this Charter even where nearby code disagrees with it. When you find such a
disagreement, do not silently imitate it and do not silently “fix” it either: follow
the Charter in the code you write, and flag the existing deviation with a
# TODO(BUG): marker or a note to the maintainer. NEVER treat “the file next door
does it this way” as authority.
3. Naming¶
Names state what a thing IS or DOES. No hedging prefixes —
maybe_,try_,do_are banned (this rule has been enforced twice already; it keeps coming back).Private helpers and attributes take a single underscore prefix; everything without one is public API and must behave like it.
Never name a variable
model: useconstitutive_model(material behaviour) ororchestration_model/uw.get_default_model()(serialization system).
# BAD: def _maybe_install_snes_update(self): ...
# GOOD: def _attach_snes_update_dispatcher(self): ...
Usually default to the mathematical naming - surface_flux_recovery is better than compute_topography. Geographic meshes might be the main exception.
5. Structure¶
DRY after the 2nd occurrence: the moment you would paste a block a second time, extract it instead. Copy-paste-as-reuse is the single worst drift pattern found in the 2026-07 audit (2–6 diverging copies of the same machinery per hotspot file).
No speculative generality: no parameters “for forward-compatibility”, no reserved branches that
pass, no dead flags. Build it when it is needed.No defensive bloat: do not guard states that cannot occur (e.g.
try/exceptaround importing a hard dependency). Guards imply the state is reachable; false guards lie.Before writing a helper, look for the existing one:
uw.function,uw.maths, andsrc/underworld3/utilities/are the discovery paths. Prefer a battle-tested package over a hand-rolled implementation.
6. API Conventions¶
These settle the June-2026 drift (see docs/reviews/2026-07/API-CONSISTENCY-REVIEW.md).
Topic |
Rule |
|---|---|
BC argument order |
|
BC value parameter |
ONE name across all BC methods: |
Direction selection |
Component masking via |
Solver capabilities |
Anything that configures or reads one solver is a METHOD on that solver, lazily importing its |
Namespaces |
Every user-facing module is exported from its subpackage |
Docstrings |
NumPy/Sphinx style with RST |
7. Data Access¶
Variable data in new code uses the
arrayproperty with the three-index shapes: scalars(N, 1, 1), vectors(N, 1, dim), tensors(N, dim, dim).Mesh coordinates are
mesh.X.coords.NEVER in new code:
with mesh.access(...)/with swarm.access(...),mesh.data, ormesh.points. They exist only so old code keeps running.The flat
.dataproperty carries ONE sanctioned exception: it bypasses all units evaluation and re-packing, so it may be used for a raw variable-to-variable copy inside the non-dimensionalisation boundary (where values are already consistent). Any other use in new code goes through.array.
temperature.array[:, 0, 0] = values # GOOD — scalar, three indices
temperature.data[:, 0] = values # BAD — compatibility layer in new code
8. Tests¶
Every bug fix ships the regression test that would have caught the bug — written first, shown to fail, then fixed.
Test files follow
tests/test_NNNN_description.pynumbering and carry both markers: a level (level_1/level_2/level_3) and a tier (tier_a/tier_b/tier_c).Validate a new test’s own correctness before changing library code to satisfy it.
NOTE: test tiers A,B,C … A are the hardened tests that have been explicitly reviewed. You can build code around tier A tests, but tier C are tests that are not mature enough to drive coding.
9. Scope Discipline for AI Sessions¶
Fix what you were asked to fix. No drive-by refactors, renames, or “while I was here”
cleanups — they turn a reviewable diff into an unreviewable one and have been the
main vector of drift. When you find a real problem outside your task, mark the exact
location with the project’s # TODO(BUG): description format (see CLAUDE.md) and
mention it in your report; do not fix it silently.
11. Things to Remember¶
Underworld is a parallel code — no feature is complete if it only works in serial.
Underworld functions are a better choice than PETSc, PETSc is a better choice than MPI, and serial libraries like scipy are usually a poor choice, generally only for prototypes. Do not leave them in code without checking.
Examples:
uw.printoruw.timingare better than PETSc because they wrap the best of the PETSc tools in a uw-styled interface. Avoid.npzor.csvlogging when we have good parallel output that is properly flushed.The user should never see parallel / mpi calls.
uw.mpi.rankormpi.rankis a mistake — there should be a wrapper already and it will work better. Guide the users!Scripts and drivers written during development sessions use uw’s own machinery, not hand-rolled equivalents. Option parsing is the canonical example: use
uw.Params/Param(notebook-editable defaults, units-aware,-uw_name valueCLI overrides via PETSc options) — notargparse, not ad-hocsys.argvhandling, not a config dict at the top of the file. A session script is a model a reader should be able to run and read; the founding rule applies to it in full.
4. Comments¶
Comments state the physical or mathematical intent and the constraints the code cannot show: why this term is in the weak form, why this barrier must precede that collective, why a tolerance has this value.
Never narrate mechanics (
# increment i), never address the reviewer (# as requested), never leave editorial musings (# why is this here??) — file those as# TODO(DESIGN):or delete them.Delete commented-out code. Git remembers; the reader should not have to. Leave a TODO if there is reason to look up the deleted code for another time.
Every intentional exception swallow states its sanctioned failure mode on the same block. A bare
except Exception: passwith no comment is a defect.Refer to methods by name, never by line number — line references rot within weeks.