Repository navigation
Interpolate #515
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Interpolate #515
Changes from 1 commit
3bb56de
50b498e
e5945ca
0dd3061
40edf43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -955,8 +955,9 @@ def _(self, o: Expr) -> Expr: | |
|
|
||
| Represent grad(grad(f)) as Grad(Grad(f)). | ||
| """ | ||
| # Check that o is a "differential terminal" | ||
| if not isinstance(o.ufl_operands[0], Grad | Terminal): | ||
| # Check that o is a "differential terminal". A base form operator is | ||
| # one too: the handler above leaves `grad(N)` unevaluated. | ||
| if not isinstance(o.ufl_operands[0], Grad | Terminal | BaseFormOperator): | ||
| raise ValueError("Expecting only grads applied to a terminal.") | ||
| return Grad(o) | ||
|
|
||
|
|
@@ -1274,11 +1275,13 @@ def __init__( | |
| compress: bool | None = True, | ||
| visited_cache: dict[tuple, Expr | BaseForm] | None = None, | ||
| result_cache: dict[Expr | BaseForm, Expr | BaseForm] | None = None, | ||
| differentiate_through_operators: bool = False, | ||
| ) -> None: | ||
| """Initialise.""" | ||
| super().__init__( | ||
| (), compress=compress, visited_cache=visited_cache, result_cache=result_cache | ||
| ) | ||
| self._differentiate_through_operators = differentiate_through_operators | ||
| # Type checking | ||
| if not isinstance(coefficients, ExprList): | ||
| raise ValueError("Expecting a ExprList of coefficients.") | ||
|
|
@@ -1593,6 +1596,20 @@ def compute_gprimeterm(ngrads, vval, vcomp, wshape, wcomp): | |
| else: | ||
| gprimesum += prod | ||
|
|
||
| if isinstance(o, BaseFormOperator) and isinstance(gprimesum, Zero): | ||
| # `grad(N)` is differentiated as if `N` were a Coefficient, so the | ||
| # partial derivative above is zero. The chain rule through `N` must | ||
| # not be dropped: either push the derivative into the operator, or | ||
| # record it so that `apply_derivatives` adds the | ||
| # Action(dF/dN, dN/dw) term. | ||
| if self._differentiate_through_operators: | ||
| do = self(o) | ||
| # A zero carries no domain, so it must not be wrapped in `Grad`. | ||
| if isinstance(do, Zero): | ||
| return Zero(g.ufl_shape) | ||
| return apply_grads(do) | ||
| self.pending_operations += (o,) | ||
|
|
||
| return gprimesum | ||
|
|
||
| @process.register(CoordinateDerivative) | ||
|
|
@@ -1614,8 +1631,22 @@ def _(self, o: BaseFormOperator, *dfs) -> Expr: | |
| """ | ||
| d_coeff = self._process_coefficient(o) | ||
| # It also handles the non-scalar case | ||
| if d_coeff == 0: | ||
| self.pending_operations += (o,) | ||
| if d_coeff != 0: | ||
| return d_coeff | ||
|
|
||
| target_space = getattr(o, "target_space", None) | ||
| if self._differentiate_through_operators and target_space is not None: | ||
| # A base form operator is linear in its operand, so the Gateaux | ||
| # derivative commutes with it: D_w[v](N(g(w))) = N(D_w[v](g(w))). | ||
| # Rebuilding on the target space rather than the existing Coargument | ||
| # lets the operator number the Coargument away from the new | ||
| # argument, which higher derivatives need. | ||
| (df,) = dfs | ||
| if isinstance(df, Zero): | ||
| return Zero(o.ufl_shape) | ||
| return o._ufl_expr_reconstruct_(df, v=target_space()) | ||
|
|
||
| self.pending_operations += (o,) | ||
| return d_coeff | ||
|
|
||
| # -- Handlers for BaseForm objects -- # | ||
|
|
@@ -1780,9 +1811,11 @@ def __init__( | |
| compress: bool | None = True, | ||
| visited_cache: dict[tuple, Expr | BaseForm] | None = None, | ||
| result_cache: dict[Expr | BaseForm, Expr | BaseForm] | None = None, | ||
| differentiate_through_operators: bool = False, | ||
| ) -> None: | ||
| """Initialise.""" | ||
| super().__init__(compress=compress, visited_cache=visited_cache, result_cache=result_cache) | ||
| self._differentiate_through_operators = differentiate_through_operators | ||
| # Record the operations delayed to the derivative expansion phase: | ||
| # Example: dN(u)/du where `N` is a BaseFormOperator and `u` a Coefficient | ||
| self.pending_operations = () | ||
|
|
@@ -1860,7 +1893,12 @@ def _(self, o: CoefficientDerivative, f: Expr | BaseForm) -> Expr | BaseForm: | |
| # operations | ||
| dag_traverser = self._dag_traverser_cache.setdefault( | ||
| key, | ||
| GateauxDerivativeRuleset(w, v, cd), # type: ignore | ||
| GateauxDerivativeRuleset( # type: ignore | ||
| w, | ||
| v, | ||
| cd, # type: ignore | ||
| differentiate_through_operators=self._differentiate_through_operators, | ||
| ), | ||
| ) | ||
| # If f has been seen by the traverser, it immediately returns | ||
| # the cached value. | ||
|
|
@@ -1996,18 +2034,26 @@ def __iadd__(self, other): | |
| return self | ||
|
|
||
|
|
||
| def apply_derivatives(expression): | ||
| def apply_derivatives(expression, differentiate_through_operators: bool = False): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand. Is |
||
| """Apply derivatives to an expression. | ||
|
|
||
| Args: | ||
| expression: A Form, an Expr or a BaseFormOperator to be differentiated | ||
| differentiate_through_operators: If True, differentiate a base form | ||
| operator by differentiating its operand, which is exact because the | ||
| operator is linear in it, instead of recording the derivative as an | ||
| action to be assembled separately. This composes to higher | ||
| derivatives and keeps the result an ordinary form, which a form | ||
| compiler that evaluates the operator cell-locally needs. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this needed to enable |
||
|
|
||
| Returns: | ||
| A differentiated expression | ||
| """ | ||
| # Notation: Let `var` be the thing we are differentating with respect to. | ||
|
|
||
| dag_traverser = DerivativeRuleDispatcher() | ||
| dag_traverser = DerivativeRuleDispatcher( | ||
| differentiate_through_operators=differentiate_through_operators | ||
| ) | ||
|
|
||
| # If we hit a base form operator (bfo), then if `var` is: | ||
| # - a BaseFormOperator → Return `d(expression)/dw` where `w` is | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| from ufl.classes import ( | ||
| Grad, | ||
| Indexed, | ||
| Interpolate, | ||
| NegativeRestricted, | ||
| PositiveRestricted, | ||
| ReferenceGrad, | ||
|
|
@@ -32,11 +33,21 @@ | |
| } | ||
|
|
||
|
|
||
| def _is_modifiable(expr): | ||
| """Check whether modifiers may be applied to ``expr``. | ||
|
|
||
| An interpolation is modifiable like a terminal: it is a finite element field | ||
| of its target space, and the expression it interpolates is evaluated | ||
| separately. | ||
| """ | ||
| return expr._ufl_is_terminal_ or isinstance(expr, Interpolate) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this pattern repeats through other files. I don't agree with the name. In which sense are terminals "modifiable"? |
||
|
|
||
|
|
||
| def balance_modified_terminal(expr): | ||
| """Balance modified terminal.""" | ||
| # NB! Assuming e.g. grad(cell_avg(expr)) does not occur, | ||
| # i.e. it is simplified to 0 immediately. | ||
| if expr._ufl_is_terminal_: | ||
| if _is_modifiable(expr): | ||
| return expr | ||
|
|
||
| assert expr._ufl_is_terminal_modifier_ | ||
|
|
@@ -45,12 +56,12 @@ def balance_modified_terminal(expr): | |
|
|
||
| # Build list of modifier layers | ||
| layers = [expr] | ||
| while not expr._ufl_is_terminal_: | ||
| while not _is_modifiable(expr): | ||
| assert expr._ufl_is_terminal_modifier_ | ||
| expr = expr.ufl_operands[0] | ||
| layers.append(expr) | ||
| assert layers[-1] is expr | ||
| assert expr._ufl_is_terminal_ | ||
| assert _is_modifiable(expr) | ||
|
|
||
| # Apply modifiers in order | ||
| layers = sorted(layers[:-1], key=lambda e: modifier_precedence[e._ufl_handler_name_]) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,7 +55,9 @@ def __init__(self, expr, v): | |
| if isinstance(v, AbstractFunctionSpace): | ||
| if is_dual(v): | ||
| raise ValueError("Expecting a primal function space.") | ||
| n = 1 if expr_arg_numbers == {0} else 0 | ||
| # Take the lowest free number, so that an expression linear in | ||
| # several arguments stays representable. | ||
| n = min(set(range(len(expr_arg_numbers) + 1)) - expr_arg_numbers) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should add tests for this |
||
| v = Argument(v.dual(), n) | ||
| dual_arg_numbers = {n} | ||
| elif isinstance(v, BaseForm): | ||
|
|
@@ -66,7 +68,8 @@ def __init__(self, expr, v): | |
| # Check valid argument numbering | ||
| if expr_arg_numbers & dual_arg_numbers: | ||
| raise ValueError("Same argument numbers in first and second operands to interpolate.") | ||
| if expr_arg_numbers | dual_arg_numbers not in [set(), {0}, {0, 1}]: | ||
| argument_numbers = expr_arg_numbers | dual_arg_numbers | ||
| if argument_numbers and argument_numbers != set(range(len(argument_numbers))): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need this check? I think the main purpose is to ensure unique argument numbers, but we should not be constraining the arguments to always start from 0 and be contiguous. |
||
| raise ValueError("Non-contiguous argument numbers in interpolate.") | ||
|
|
||
| # Reversed order convention | ||
|
|
@@ -155,6 +158,15 @@ def signature(slot): | |
| self._signature = hashlib.sha512(str(signatures).encode("utf-8")).hexdigest() | ||
| return self._signature | ||
|
|
||
| def target_space(self) -> AbstractFunctionSpace: | ||
| """Return the function space that the expression is interpolated into. | ||
|
|
||
| Unlike `ufl_function_space` this does not depend on the argument | ||
| numbering, so it stays correct when the interpolated expression holds an | ||
| argument of the enclosing form. | ||
| """ | ||
| return self._function_space | ||
|
Comment on lines
+161
to
+168
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this mean for the adjoint, i.e. |
||
|
|
||
| def ufl_element(self) -> AbstractFiniteElement: | ||
| """Return the target finite element.""" | ||
| return self._function_space.ufl_element() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this
pending_operationsnonsense is removed in #544