Skip to content

SNES_Scalar still assembles its tangent with derive_by_array: the #457 transpose survives in the one solver #493 did not convert #747

Description

@lmoresi

#493 fixed #457 by replacing derive_by_array + permutedims with explicit-index
loops. It converted SNES_Vector and SNES_Stokes_SaddlePt. SNES_Scalar was
not converted
and still carries the same construction, and the same transpose.

It is latent, not active — nothing reachable through the public API hits it
today. But it is latent for the same reason #457 was invisible for months, which
is why it is worth closing rather than leaving.

The transpose, demonstrated

petsc_generic_snes_solvers.pyx ~3732 assigns the result straight to _G3
with no reordering:

G3 = sympy.derive_by_array(F1_jac, L)
...
self._G3 = sympy.ImmutableMatrix(G3)

derive_by_array is dx-first, so for a non-symmetric flux tensor:

F1 = [L0 + 7*L1,  3*L0 + 2*L1]                  # k = [[1,7],[3,2]]
ImmutableMatrix(derive_by_array(F1, L)) = [[1, 3], [7, 2]]    <- k TRANSPOSED
g3[i][j] should be dF1[i]/dL[j]         = [[1, 7], [3, 2]]

Same root cause as #457: derive_by_array leads with the derivative indices, so
the assembled tangent is the transpose of the one PETSc's [fc, gc, df, dg]
layout expects.

Why nothing fails today

The only anisotropic scalar model, AnisotropicDiffusionModel, builds a
diagonal tensor — sympy.diag(*validated) from a (dim,1)/(1,dim)
vector. A diagonal matrix is its own transpose, so the index order cannot show.
Verified on the supported path: kappa = [1, 7] gives
_G3 = [[1, 0], [0, 7]], correct entry by entry.

That is precisely how #457 hid in Stokes — invisible under frozen C, isotropic
eta(edot) Newton and linear transverse isotropy, wrong exactly when the
tangent lost its major symmetry.

A second failure mode in the same idiom

Feeding a full (non-diagonal) tensor to the scalar DiffusionModel — misuse of
that model, but it is what a user reaches for first — does not raise a useful
error. The diffusivity is carried as one opaque symbol kappa, and
derive_by_array then returns an array that lies about its shape:

F1.shape (2,)   backing len 2
G3.shape (2, 2) backing len 2      <- should be 4

.shape and contents disagree, nothing checks, and it surfaces three frames
later as a bare IndexError: list index out of range inside
sympy.ImmutableMatrix. The traceback names sympy internals and not the
diffusivity, so the cause is not recoverable from the error.

So the one idiom carries three hazards: dx-first index order, shape/content
divergence with no check, and silent success whenever the tangent happens to be
symmetric.

Suggested fix

Convert SNES_Scalar to the explicit-index loops already used by the other two
solvers, finishing #493. The layout contract is
docs/developer/subsystems/petsc-jacobian-layout.md.

The regression test needs a full tensor to exist first. A diagonal one
cannot catch a transpose. That means either AnisotropicDiffusionModel accepts
a full (dim, dim) conductivity, or the test builds F1 directly. The former
is worth having on its own account — a foliation- or fault-aligned conductivity
is a real modelling case and is the direction this project is heading — and it
would make the defect reachable, so it should land with the fix rather than
before it.

Found while checking that #457 was genuinely closed (it is). Related: #457, PR
#493, and Charter §8 on baselines tight enough to fail when a default changes.

Underworld development team with AI support from Claude Code

Activity

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