Skip to content

Cancel contractions of the Jacobian with its inverse#496

Open
pbrubeck wants to merge 3 commits into
FEniCS:mainfrom
firedrakeproject:pbrubeck/cancel-jacobian
Open

Cancel contractions of the Jacobian with its inverse#496
pbrubeck wants to merge 3 commits into
FEniCS:mainfrom
firedrakeproject:pbrubeck/cancel-jacobian

Conversation

@pbrubeck

Copy link
Copy Markdown
Contributor

Description

Piola-mapped elements generate expressions of the form J * inv(J) that were not being simplified, because JacobianInverse was expanded into individual matrix entries too early in the form preprocessing.

This PR delays that expansion: during geometry lowering, Jacobian, JacobianInverse, and JacobianDeterminant are preserved as opaque terminals (which also lets is_cellwise_constant fold their derivatives to zero early on affine cells). A new algorithm module cancel_jacobian_products then simplifies the integrand in three traversals:

  1. JacobianCanceller: IndexSum(J[a, k] * K[k, b] * factors, k) -> Identity[a, b] * factors (and the K·J order, which is also valid for pseudo-inverses on immersed manifolds). Contractions are chased through nested IndexSums by interchanging the order of summation.
  2. IdentityEliminator: IndexSum(Identity[a, k] * factors, k) -> factors[k -> a], and folds fixed-index Identity entries into scalar constants. This also cleans up user-written Identity contractions.
  3. ReciprocalCanceller: cancels reciprocal factors within products, such as detJ**2 * (1/detJ)**2 -> 1.

The surviving Jacobian quantities are lowered as usual afterwards, so form compilers see no new terminal types. The behaviour is opt-in via a new compute_form_data kwarg do_cancel_jacobian_products (default off, so ffcx is unaffected).

For the Johnson-Mercier element, the div-div integrand shrinks from 9630 to 664 characters, becoming literally (1/detJ**2) J : rdiv(sigma) on affine cells. Together with companion GEM changes this gives up to -42% flops and -51% compile time (see companion PRs in firedrakeproject/fiat and firedrakeproject/firedrake).

🤖 Generated with Claude Code

pbrubeck added 2 commits July 10, 2026 00:40
Preserve Jacobian, JacobianInverse and JacobianDeterminant during
geometry lowering, and cancel J-Jinv contractions into Kronecker
deltas before the inverse is expanded into individual matrix entries.
Eliminate the resulting Identity tensors by contraction against the
remaining factors, and cancel reciprocal factors within products,
such as detJ**2 * (1/detJ)**2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IndexSumSimplifier (and its JacobianCanceller/IdentityEliminator
subclasses) and ReciprocalCanceller were plain MultiFunctions; convert
them to DAGTraverser, matching the convention used by the rest of the
newer algorithms modules (apply_derivatives.py,
apply_coefficient_split.py). This switches the driver function from
map_integrand_dags (the MultiFunction-compatible entry point) to
map_integrands, since a DAGTraverser is called directly rather than
through map_expr_dag.

Add test/test_cancel_jacobian_products.py, covering JacobianCanceller
and IdentityEliminator on both contraction orders (J.K and K.J),
ReciprocalCanceller on both a fully- and a partially-cancelling
reciprocal product, the full pass end-to-end on a div-div form over a
contravariant Piola-mapped (Raviart-Thomas) element (verifying both
the size reduction and that the opt-in flag defaults to off), and a
no-op check on a form with no Jacobian contractions to cancel.
Comment thread test/test_cancel_jacobian_products.py Outdated
The div-div test only checked that the cancelled integrand's string
representation was shorter than the uncancelled one -- a check that
would pass even for a wrong simplification, as long as it happened to
produce something smaller.

Replace it with two meaningful checks against the intermediate result
cancel_jacobian_products actually produces (before further geometry
lowering, which erases Jacobian/JacobianInverse regardless of whether
cancellation ran):
- extract_type confirms Jacobian/JacobianInverse are fully eliminated,
  and that JacobianDeterminant (which cannot cancel further on its
  own here) correctly survives.
- The result is compared, via renumber_indices (independently built
  expressions carry differently-numbered dummy indices), against a
  hand-coded expected result: the classical Piola divergence identity
  div(w) = (1/detJ) * ref_div(w), derived independently of
  cancel_jacobian_products itself.
@pbrubeck pbrubeck marked this pull request as ready for review July 11, 2026 12:55
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.

1 participant