Give each pull back an inverse - #511
Merged
jorgensd merged 17 commits intoSep 11, 2026
Merged
Conversation
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 8, 2026
TSFC mapped an interpolation's operand onto the target element's reference cell in a pass of its own, run before compute_form_data, and carried its own copy of the inverse Piola maps to do it. UFL does both now, so this deletes apply_mapping, InterpolatePullbackApplier and the pre-pass in the driver, and preprocess_interpolate calls ufl.apply_inverse_pullback for the standalone case. Needs FEniCS/ufl#511 and FEniCS/ufl#512. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155wQMTMSGiF9a6TbyTVK5P
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 8, 2026
pbrubeck/interpolate-reference-lowering is pbrubeck/interpolate-holes plus FEniCS/ufl#511 and FEniCS/ufl#512. Revert to the plain companion branch once those merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155wQMTMSGiF9a6TbyTVK5P
This was referenced Sep 8, 2026
pbrubeck
force-pushed
the
pbrubeck/pullback-apply-inverse
branch
from
September 8, 2026 01:12
b958e89 to
3ea4e9d
Compare
Member
|
Im positive to this change as I also had to handroll the inverses as some point;) will try to review on Thursday |
A base form operator's argument slots are not among its operands, so traversing the operands misses the domains they are defined over. An interpolation onto a point cloud is the case that notices: the target is reachable only through the dual argument, and the form compiler numbers its domains against this list. extract_unique_domain still answers with the operand's domain, which is where the operator takes its value. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BaseForm requires _analyze_domains and ufl_domains of its subclasses, and BaseFormOperator never supplied them: Interpolate carried the pair alone, so every other operator fell through to the deprecated Expr.ufl_domains, which answers by calling extract_domains and recurses forever now that extract_domains routes a base form operator back through it. Differentiating an interpolation is the case that notices, as BaseFormOperatorDerivative has no such override. The analysis reads ufl_operands and argument_slots, both of which every base form operator has, so it belongs on the base class unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Interpolate.__neg__ chose between the expression and the form negation by testing is_dual(self._function_space), the interpolation's target space. That space stays primal even when the interpolation is itself a form, so negating an Interpolate built against a Cofunction gave a Product while multiplying the same object by -1 gave a FormSum. None of the other arithmetic operators consult the target space. __add__, __radd__, __mul__ and __rmul__ all dispatch through _parent_type, which tests is_dual(self.ufl_function_space()) -- the space the operator takes its value in, and the one that is dual for a form. Routing __neg__ the same way makes negation and multiplication by -1 agree by construction. Firedrake's fml is what notices: it builds `form - label(form)` and compares the result against `-form`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014JYLXVECzcJXgt9gieUbon
The mixed-coefficient split dict construction was inlined in FormData.__init__ and duplicated by tsfc/driver.py::compile_interpolate. Neither needs Form/Integral machinery for it, so it becomes a standalone helper in apply_coefficient_split, callable by both. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFoiWhAUbP53byrgoqxx7H
pbrubeck
force-pushed
the
pbrubeck/pullback-apply-inverse
branch
from
September 8, 2026 10:51
3ea4e9d to
6f6b47d
Compare
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 9, 2026
TSFC mapped an interpolation's operand onto the target element's reference cell in a pass of its own, run before compute_form_data, and carried its own copy of the inverse Piola maps to do it. UFL does both now, so this deletes apply_mapping, InterpolatePullbackApplier and the pre-pass in the driver, and preprocess_interpolate calls ufl.apply_inverse_pullback for the standalone case. Needs FEniCS/ufl#511 and FEniCS/ufl#512. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155wQMTMSGiF9a6TbyTVK5P
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 9, 2026
This PR needs firedrakeproject/fiat#261 and the UFL work in FEniCS/ufl#511 and FEniCS/ufl#512, none of which has merged, so CI installs both branches over the pins in pyproject.toml. Drop this commit once they land; nothing else on the branch touches .github. The step runs after Firedrake is installed, so that resolving its dependencies cannot pull fenics-ufl back from FEniCS/ufl@main over the branch, and calls firedrake-clean so no kernel cached against the released UFL survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 9, 2026
This PR needs firedrakeproject/fiat#261 and the UFL work in FEniCS/ufl#511 and FEniCS/ufl#512, none of which has merged, so CI installs both branches over the pins in pyproject.toml. Drop this commit once they land; nothing else on the branch touches .github. The step runs after Firedrake is installed, so that resolving its dependencies cannot pull fenics-ufl back from FEniCS/ufl@main over the branch, and calls firedrake-clean so no kernel cached against the released UFL survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 9, 2026
This PR needs firedrakeproject/fiat#261 and the UFL work in FEniCS/ufl#511 and FEniCS/ufl#512, none of which has merged, so CI installs both branches over the pins in pyproject.toml. Drop this commit once they land; nothing else on the branch touches .github. Both installs sit inside the 'Install Firedrake' step, ahead of the Firedrake install rather than after it: that step ends with firedrake-clean, which imports Firedrake, and Firedrake cannot be imported against the released UFL. A step of its own after the install therefore never gets to run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pbrubeck
added a commit
to firedrakeproject/firedrake
that referenced
this pull request
Sep 10, 2026
This PR needs firedrakeproject/fiat#294 and the UFL work in FEniCS/ufl#511 and FEniCS/ufl#512, none of which has merged, so CI installs both branches over the pins in pyproject.toml. Drop this commit once they land; nothing else on the branch touches .github. Both installs sit inside the Install Firedrake step, ahead of the Firedrake install rather than after it: that step ends with firedrake-clean, which imports Firedrake, and Firedrake cannot be imported against the released dependencies. A step of its own after the install therefore never gets to run.
pbrubeck
force-pushed
the
pbrubeck/pullback-apply-inverse
branch
from
September 10, 2026 23:09
6f6b47d to
3a43502
Compare
This reverts commit f98822a. "Fix shape and negation" gave Interpolate both a ufl_shape and a __neg__ keyed off _function_space, the interpolation target. The target is primal even when the interpolation carries a test function, so negating one stayed an expression. f98822a rerouted __neg__ through _parent_type, which keys off ufl_function_space() -- the adjoint's source dual -- and left ufl_shape on the target. Negation then returned a FormSum whose ufl_shape does not exist, so any integrand subtracting an interpolated test function raised AttributeError: inner(grad(w) - Interpolate(beta, R), grad(v) - Interpolate(theta, R)) which is the MITC reduction operator applied to both the trial and the test function. Bisecting that expression across this stack puts the first failure exactly at f98822a. _parent_type still drives __add__, __radd__, __mul__ and __rmul__, which test_interpolate_expr requires: a sum of adjoint interpolations has to stay a FormSum so it can be passed as the second argument to Interpolate. Only negation belongs on the target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYN9RQAEu7DCRYNkYQGSkW
Negation and scalar multiplication disagreed for an interpolation of a test function: __neg__ followed _function_space, the target, while __rmul__ went through _parent_type and so followed ufl_function_space(), the adjoint's source dual. -Interpolate(v, R) was a Product and -1*Interpolate(v, R) a FormSum. f98822a closed that gap by moving __neg__ onto the form side. That direction loses the expression reading: FormSum has no ufl_shape, so an integrand that subtracts an interpolated test function stopped building. Close it from the other side instead, and let scaling follow the target, which is the space ufl_shape already reports. _parent_type still drives __add__ and __radd__, where a sum of adjoint interpolations has to stay a FormSum for test_interpolate_expr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYN9RQAEu7DCRYNkYQGSkW
The two disagreed for an interpolation of a test function, and nothing covered it: test_shape_and_negation only checked negation. Assert that -I, -1*I and I*-1 give the same type for a trial and a test operand, that scaling keeps the value shape a FormSum would not have, and that addition still returns a FormSum, which is the asymmetry test_interpolate_expr depends on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JYN9RQAEu7DCRYNkYQGSkW
A pull back maps a function on the reference cell to the physical cell. Nothing in UFL went the other way, so a form compiler that has to evaluate a physical expression on the reference cell had to carry its own copy of the maps. apply_inverse is the inverse of apply, for every pull back that has one. PhysicalPullback and CustomPullback are their own inverse, and UndefinedPullback inherits the refusal from AbstractPullback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155wQMTMSGiF9a6TbyTVK5P
pbrubeck
force-pushed
the
pbrubeck/pullback-apply-inverse
branch
from
September 10, 2026 23:40
3a43502 to
fbdb7d7
Compare
jorgensd
approved these changes
Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A pull back maps a function on the reference cell to the physical cell. UFL had no map in the other direction, so a form compiler that has to evaluate a physical expression on the reference cell — to dual-evaluate an
Interpolate, for instance — carried its own copy of the Piola maps. TSFC has one.This adds
apply_inversetoAbstractPullbackand implements it for every pull back that has an inverse:applyapply_inverseIdentityPullbackggContravariantPiola(1/detJ) J gdetJ K gCovariantPiolaKᵀ gJᵀ gL2Piolag / detJg detJDoubleContravariantPiola(1/detJ)² J g JᵀdetJ² K g KᵀDoubleCovariantPiolaKᵀ g KJᵀ g JCovariantContravariantPiola(1/detJ) Kᵀ g JᵀdetJ Jᵀ g KᵀMixedPullbackSymmetricPullbackPhysicalPullbackandCustomPullbackare their own inverse.UndefinedPullbackinherits theNonStandardPullbackExceptionfromAbstractPullback, as it does forapply.The test asserts the property that matters rather than the formulas: pushing a reference function forward and mapping it back returns the original, once
remove_component_tensorsandcancel_jacobian_productshave cancelled the Jacobian products the round trip leaves behind. That closes exactly for eight of the nine, mixed and symmetric elements included. TheL2Piolaround trip leavesdetJ / detJstanding, because cancelling a scalar factor is not somethingcancel_jacobian_productsdoes, so that one is asserted directly instead.Stacking
pbrubeck/pullback-apply-inversesits onpbrubeck/interpolate-holes(#497), so that it pairs withpbrubeck/form-interp-tsfcin Firedrake. That one branch is also the head of #511, which proposes the same commit upstream; #497 is in that diff for the same reason.The pull back work itself does not depend on #497 and can be read on its own.
Nothing calls
apply_inverseyet. #69 uses it to lower interpolation incompute_form_data, which lets TSFC delete its copy of the maps.🤖 Generated with Claude Code
https://claude.ai/code/session_0155wQMTMSGiF9a6TbyTVK5P