Skip to content

Fix thermal convection units tutorial#263

Open
ss2098 wants to merge 2 commits into
underworldcode:developmentfrom
ss2098:ss2098/fix-thermal-convection-units-tutorial
Open

Fix thermal convection units tutorial#263
ss2098 wants to merge 2 commits into
underworldcode:developmentfrom
ss2098:ss2098/fix-thermal-convection-units-tutorial

Conversation

@ss2098

@ss2098 ss2098 commented Jun 20, 2026

Copy link
Copy Markdown

This updates docs/examples/Tutorial_Thermal_Convection_Units.py so that it runs with the current unit-aware array and strict-units behavior.

The previous tutorial failed in several places under the current API:

  1. The buoyancy expression mixed a MeshVariable symbolic field with a unit-bearing reference temperature, producing a dimensional compatibility error.
  2. The initial temperature assignment attempted to assign unit-aware evaluate() output directly into MeshVariable storage, which required explicit conversion to a plain numeric array.
  3. The semi-Lagrangian advection step encountered incompatible coordinate and timestep units unless the velocity field carried physical velocity units.
  4. The final reporting attempted to convert model-unit temperature quantities directly to offset Celsius units, which raised a Pint OffsetUnitCalculusError.
  5. The initial temperature profile could become incorrectly scaled because unit-aware coordinates needed to be normalized before constructing the model-unit temperature field.

The updated tutorial keeps the physical inputs in units, but uses explicit model-unit numeric values in solver expressions. It also converts unit-aware arrays before assignment, constructs the initial temperature field from normalized coordinates, and reports original physical parameters for interpretation.

No solver behavior is changed.

Tested with:

pixi run python docs/examples/Tutorial_Thermal_Convection_Units.py

Result: script completes successfully.

@ss2098 ss2098 requested a review from lmoresi as a code owner June 20, 2026 07:49
@lmoresi

lmoresi commented Jun 22, 2026

Copy link
Copy Markdown
Member

Holding this one — it exposes a real units + semi-Lagrangian bug

Thanks @ss2098 — the setup half of the tutorial is correctly updated (it gets all the way through the Rayleigh-number / scaling section). But I ran it end-to-end on current development (note: CI does not execute docs/examples/, so green checks don't cover this), and it fails at the time-stepping loop:

Running 10 coupled Stokes + thermal steps...
  thermal.solve(timestep=dt, ...)
  -> DuDt.update_pre_solve(...)            # ddt.py:2651
  ValueError: Cannot subtract arrays with incompatible units: 'meter' and 'meter / second'

Root cause (core, not tutorial)

The SL trace-back midpoint is coords - v * (0.5*dt) (ddt.py:2651). In the units-active path, coords is UnitAwareArray[meter] and v is [meter/second], but dt arrives as a plain float and is not dimensionalised (ddt.py:2519):

if has_units:
    if hasattr(dt, "to"):
        dt_for_calc = dt.to("second")
    else:
        dt_for_calc = dt          # comment says "treat as seconds" but leaves it unitless

so v[m/s] * dt[dimensionless] = [m/s], and [m] - [m/s] raises. Simply attaching seconds here would fix the units but be numerically wrong — the tutorial's CFL dt is in nondimensional model-time, not SI seconds, so it needs the model time-scale (the not has_units branch just below does the consistent thing: nondimensionalise coords, velocity and dt together).

So the tutorial can't run until the units-active SL trace-back consistently reconciles dt with the dimensional coords/velocity. This is a genuine UW3 bug the tutorial usefully surfaced, not something to patch in the example. I'll file it separately and link it here; let's hold this PR until that lands (then this tutorial should be the natural regression test for it).

Everything else in the PR (the setup/units fixes) looks good and can ride along once the core fix is in.

@lmoresi

lmoresi commented Jun 22, 2026

Copy link
Copy Markdown
Member

Filed the underlying bug: #267 — holding this PR until that lands.

@ss2098

ss2098 commented Jun 22, 2026

Copy link
Copy Markdown
Author

Thanks Professor @lmoresi , this makes sense. I will leave PR #263 open and on hold until #267 is fixed.

I agree that this should be handled in the semi-Lagrangian units path rather than patched only in the tutorial. Once the core fix lands, I can rebase the tutorial branch on current development, rerun Tutorial_Thermal_Convection_Units.py end-to-end, and update the PR if needed.

Thanks for identifying and filing the underlying issue.

lmoresi added a commit that referenced this pull request Jun 24, 2026
SemiLagrangian.update_pre_solve crashed for any unit-bearing model in the time
loop:
  ValueError: Cannot subtract arrays with incompatible units:
              'meter' and 'meter / second'

Root cause: the trace-back is performed in the mesh's NON-DIMENSIONAL (DM)
coordinate space (evaluate()/global_evaluate treat plain arrays as DM coords,
and DM point-location uses DM values 0..L_model, NOT dimensional metres). But
the has_units branch kept dimensional coords (mesh.X.coords ~ metres) and
velocity (m/s) and left dt a unitless float, so:
  - coords[m] - v[m/s]*dt[dimensionless] -> unit subtraction crash, and
  - even patched, dimensional coords (~1e9) fed to a DM that locates in 0..1000
    space would mislocate.

Fix: route the whole trace-back through ND/DM space regardless of units —
departure point from psi_star[i].coords_nd (== .coords for a non-units model;
the ND reduction of the dimensional coords when units are active), velocity
non-dimensionalised, dt non-dimensional. Both the node-velocity and
midpoint-velocity legs are corrected. The non-units path is unchanged (it
already used exactly this ND logic).

Verified: units-active AdvDiffusionSLCN now runs and tracks the equivalent
non-dimensional run to ~1e-3 (the small residual is the constitutive diffusivity
scaling under units, a separate concern, not the trace-back). Non-units SL
regression unchanged (test_0855/0610/1054/1100 pass; test_1110 fails identically
with/without this change — a pre-existing MG DMCreateInjection issue). New
regression: tests/test_1056_units_slcn_traceback.py.

Unblocks the time-loop half of #263 (Tutorial_Thermal_Convection_Units).

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Jun 24, 2026

Copy link
Copy Markdown
Member

Unblocked: the underlying SL trace-back bug is fixed on development (#277 / #267). The time-loop crash this tutorial hit is resolved. Please rebase this PR on current development and re-run the tutorial end-to-end — the time-stepping section should now complete. Once it runs clean, this is good to merge (and it'll serve as the natural integration test for #277).

@ss2098 ss2098 force-pushed the ss2098/fix-thermal-convection-units-tutorial branch from 374d1e6 to 5fbbc0f Compare June 24, 2026 01:26
@ss2098

ss2098 commented Jun 24, 2026

Copy link
Copy Markdown
Author

Thanks Professor @lmoresi. I rebased this branch on current development after #277 landed and re-ran the tutorial end-to-end.

Tested with:

pixi run python docs/examples/Tutorial_Thermal_Convection_Units.py

The time-stepping section now completes successfully.

Result:

✅ Time-stepping completed!
🎉 SUCCESS: Physical thermal convection tutorial completed!

I have pushed the rebased branch.

@lmoresi

lmoresi commented Jun 24, 2026

Copy link
Copy Markdown
Member

Re-ran end-to-end on current development (which now has the #267 trace-back fix): good progress — the time-loop crash is gone, the tutorial gets all the way to the buoyancy force. It now hits a different, distinct units bug at buoyancy = ... * (temperature - reference_temp) (line ~202): EnhancedMeshVariable - UWQuantity fails even for compatible units. Filed as #282.

So this PR is unblocked on #267 but blocked on #282. Two options:

  1. wait for EnhancedMeshVariable ± UWQuantity fails for compatible units (blocks units-active buoyancy T − T_ref) #282 (the proper fix — meshvar ± quantity should just work), or
  2. work around it in the tutorial now by dropping to .sym + a model-unit reference value (e.g. temperature.sym[0,0] - reference_temp_model), which I verified works.

Happy to push option (2) so the tutorial merges now, or hold for #282 — your call / @ss2098's.

@ss2098

ss2098 commented Jun 24, 2026

Copy link
Copy Markdown
Author

Thanks Professor @lmoresi. I think option (2) makes sense here so the tutorial can merge now.

Since the tutorial is already using physical quantities for setup and then explicit model-unit numeric values in solver expressions, using temperature.sym[0,0] - reference_temp_model for the buoyancy term seems consistent with the current tutorial design. The deeper EnhancedMeshVariable - UWQuantity issue can then remain tracked separately in #282 as the proper core fix.

Please feel free to push that workaround, or I can update the branch on my side if you prefer.

lmoresi added a commit that referenced this pull request Jun 24, 2026
A MeshVariable's .sym lives in non-dimensional (model-unit) space, so the
MathematicalMixin arithmetic operators could not compose with a unit-bearing
scalar operand: T - uw.quantity(500, 'K') raised
  TypeError: Cannot subtract UWQuantity from EnhancedMeshVariable
even for compatible units (the Pint quantity reached sympy and failed to
sympify). This blocks the fundamental buoyancy form T - T_ref.

Add a coercion branch to the operand handling in __add__/__sub__/__rsub__/
__mul__/__rmul__/__truediv__/__rtruediv__: a UWQuantity (anything with
.magnitude and .units) is reduced to its non-dimensional value via
uw.non_dimensionalise before composing with the variable's .sym. Compatible
units (incl. prefixes like kK) reduce consistently via the model scaling; the
change is behaviour-preserving for all non-quantity operands (sympy / variable /
plain scalar hit the same branches as before — verified the sympy-scalar matrix
quirk is pre-existing, not introduced here).

Scope: variable on the LEFT (meshvar ± / * quantity). 'quantity - meshvar'
(quantity on the left) is handled by the UWQuantity class and is out of scope.

Verified: T ±/* quantity compose with exact ND values (T - 500K == T.sym - 0.5),
kK prefix consistent, no regression (test_0700/0750/0754 unchanged). Regression:
tests/test_0756_meshvar_quantity_arithmetic.py.

Helps unblock the buoyancy expression in PR #263.

Closes #282.

Underworld development team with AI support from Claude Code
lmoresi added a commit that referenced this pull request Jun 24, 2026
The Stokes bodyforce setter tried to handle UWQuantity components via
item._sympify_() — a method that does not exist on UWQuantity (_sympy_ also
raises on a dimensional quantity). So any units-active buoyancy assignment
(stokes.bodyforce = [0, ρ₀ α g (T − T_ref)]) crashed with
  AttributeError: 'UWQuantity' object has no attribute '_sympify_'.

The body force feeds the non-dimensional solver, so non_dimensionalise the
UWQuantity component (consistent with the ND<->units boundary contract and the
#282 fix). The component may be symbolic (a buoyancy carries the T field), so
non_dimensionalise yields a 1x1 array/Matrix whose element is extracted (the
existing shape-handling is preserved). Plain non-quantity components are
unchanged.

Verified: a symbolic UWQuantity buoyancy assigns to bodyforce as the correct ND
expression; test_1010 (Stokes) unchanged. Regression: tests/test_1011_bodyforce_units.py.
The second blocker (after #267/#282) for the thermal-convection units tutorial (#263).

Underworld development team with AI support from Claude Code
@lmoresi

lmoresi commented Jun 24, 2026

Copy link
Copy Markdown
Member

Units-active convection now runs — the system blockers are fixed

Re-ran on current development. The simulation reaches ✅ Time-stepping completed! (10 coupled Stokes + thermal steps). The three underlying system bugs this tutorial exposed are all fixed and merged:

  1. Semi-Lagrangian trace-back is unit-inconsistent under active units (dt not dimensionalised in has_units path) #267 — semi-Lagrangian trace-back under units (fix(ddt): units-active semi-Lagrangian trace-back (#267) #277)
  2. EnhancedMeshVariable ± UWQuantity fails for compatible units (blocks units-active buoyancy T − T_ref) #282meshvar ± UWQuantity arithmetic (fix(units): meshvar arithmetic with a UWQuantity operand (#282) #283) — the T − T_ref buoyancy
  3. bodyforce setter rejecting a UWQuantity component (fix(stokes): bodyforce setter accepts a UWQuantity component #284) — stokes.bodyforce = [0, buoyancy]

So the units-active convection path works now. What remains is tutorial-side:

  • Line 388 (and 78/79): T_model.to('degC') raises OffsetUnitCalculusError — Pint can't convert a model-scaled temperature to the offset unit °C. Use .to('K') for display, or compute °C manually as K − 273.15. (These are diagnostic prints, after the sim.)
  • Stability: mean_T grows over the 10 steps (−6 → −8873), i.e. the convection is numerically unstable — looks like the timestep (dt ≈ 4.5e13 model-time) / Rayleigh scaling needs tuning so the run stays bounded. Worth checking the CFL dt and the buoyancy magnitude against the intended Ra.

@ss2098 — once the °C prints use K (or manual −273.15) and the timestep/scaling is tuned so the run stays stable, this should be ready. The hard part (the three system bugs) is done. Happy to help with the stability tuning if useful.

@ss2098

ss2098 commented Jun 25, 2026

Copy link
Copy Markdown
Author

Professor @lmoresi, I also tested docs/examples/porous_flow/advanced/Ex_viscousFingering.py on current development and found that the migrated script currently fails in the swarm path. First swarm.populate(fill_param=4) hits a NameError for all_local_cells, and after bypassing populate the later swarm.advection path hits additional missing internal bookkeeping such as particle_X_orig and cellid.

I made a local runnable version by removing the swarm-material route and using the existing AdvDiffusionSLCN mesh-field material advection instead. That version runs end-to-end and finishes with:

Viscous fingering example complete.

However, this changes the example from swarm-tracked material advection to mesh-field advection-diffusion, so I do not want to open it as a simple bug-fix PR without checking. Would you prefer that I open this as a script-safe migration PR, or should the swarm/populate/advection issues be treated as a separate core bug first?

@ss2098 ss2098 force-pushed the ss2098/fix-thermal-convection-units-tutorial branch from 5fbbc0f to bf7921c Compare June 25, 2026 06:15
@ss2098

ss2098 commented Jun 25, 2026

Copy link
Copy Markdown
Author

Thanks Professor @lmoresi. I updated the tutorial-side issues after rebasing on current development.

Changes made:

  • Replaced direct degC conversions with Kelvin display plus manual Celsius conversion, avoiding Pint OffsetUnitCalculusError for offset units.
  • Kept the thermal timestep unit-aware so the semi-Lagrangian trace-back receives a proper time quantity.
  • Added a conservative timestep cap for the tutorial run so the 10-step demonstration remains bounded.

Tested with:

pixi run python docs/examples/Tutorial_Thermal_Convection_Units.py

Result:

✅ Time-stepping completed!
Temperature field range: 0.200 to 1.242 model units
Mean temperature: 1.047 model units
🎉 SUCCESS: Physical thermal convection tutorial completed!

I pushed the updated branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants