Skip to content

solver.sensitivity() returns dJ/dm in SI, not in the non-dimensional system: wrong by the reference scales #763

Description

@lmoresi

What

solver.sensitivity(mu, wrt) assembles mu^T dR/dm as

total = float(uw.maths.Integral(self.mesh, self.adjoint_integrand(mu, wrt)).evaluate())

Integral.evaluate() re-dimensionalises: PETSc integrates in the model's
non-dimensional space and evaluate then attaches
integrand_units * coordinate_units**dim and converts, returning a UWQuantity.
float() on that takes the magnitude in those units, while mu, the residual
templates and the parameter value the chain rule differentiates against are all
non-dimensional. The returned number therefore carries whatever reference scales the
surviving units name.

Two separate leaks:

  1. A dimensionless knob multiplying a dimensional scale. _peel_except deliberately
    keeps dimensional values as leaves (substituting them raises SympifyError). Where
    the parameter is dimensional this is harmless, because it is differentiated away; but
    a dimensionless parameter leaves the dimensional factor standing in dR/dm, so the
    integral comes back in e.g. km**2 * Pa * s.
  2. The coordinate scale, always. The integral carries coordinate_units**dim
    regardless of the parameter, so any model whose length reference is not 1 is off by
    L_ref**dim in every sensitivity. A 30 km reference is a silent factor 900.

Measured

Linear Stokes is an exact oracle: with the viscosity constant and the body force
independent of it, u ~ 1/eta, so for any J linear in u, dJ/d(eta) = -J/eta.

Rig: unit box, shear_viscosity_0 = alpha * eta with alpha dimensionless = 2 and
eta = 1e22 Pa*s; viscosity reference 1e21 Pa*s, length reference 1 km.

parameter sensitivity closed form ratio
alpha (dimensionless) 2.08501669e+14 2.08501669e-07 1.000000e+21
eta (Pa*s) 4.17003338e-08 4.17003338e-08 1.000000

1.000000e+21 is exactly the viscosity reference. The manifest is not at fault —
_pack_constants packs \\eta -> 1.000000e+01, correctly non-dimensional. The integral
itself returns 207999810040165.88 [kilometer ** 2 * pascal * second], and
uw.non_dimensionalise of that gives 2.07999810e-07, the right answer.

Where it showed up

The Spiegelman notch viscosity floor, shear_viscosity_min = m * floor * eta_bg with
floor dimensionless. dJ/d(floor) read 3.25185e+23 against a central finite
difference of -4.05e-01. eta_bg is the viscosity reference in that model
(1e24 Pa*s), which is why the corruption looked like a plausible physical scale rather
than a bug.

Sites

  • petsc_generic_snes_solvers.pyx sensitivity() — volume term and the natural-BC facet term
  • adjoint.py integral(), and TranscriptAdjoint.gradient()'s J and explicit dJ/dm

Fix

Integral.evaluate() re-dimensionalising is documented behaviour that other callers
depend on, so the conversion belongs at the adjoint boundary: uw.adjoint.nd_float(),
used at every site above. Regression test in
tests/test_0025_adjoint_sensitivity_is_non_dimensional.py, asserting against the closed
form with the length reference deliberately 30 km and the viscosity reference
deliberately different from the viscosity, so neither scale can cancel by accident.

Activity

  1. lmoresi commented on Oct 7, 2026

    @lmoresi
    MemberAuthor

    Not reproducible on development (7c9cbbfd) — the adjoint API is not there:

    uw.adjoint present:        False
    Stokes adjoint API:        []
    files with 'adjoint' in the path: 0
    

    Only scattered references remain (one line in petsc_generic_snes_solvers.pyx, five in mcp/__init__.py, eight in transcript_query.py), not the implementation. So this defect lives in unmerged work — feature/discrete-adjoint (#744, currently conflicting and red), feature/adjoint-rotated-bc (#751, based on #744), or scratch/adjoint-merge.

    That is why it has sat untouched: there is nothing on the main line to fix. It is not fixed-in-PR — the opposite. It has to be fixed on the branch before that branch lands, or it lands with the defect.

    Checked in the untouched-issue triage. Blocked behind #744, which needs its conflicts resolved and a CI re-run (its red check is test_1063_constrained_traction, whose timeout #814 addressed when it merged on 2026-10-06).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions