-
-
Notifications
You must be signed in to change notification settings - Fork 68
Ksagiyam/add DAGTraverser #365
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?
Conversation
bffe57a
to
b99a902
Compare
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.
I think this is pretty great. I have written a lot of tree traversals and the DAGTraverser.postorder
(and friends) solves an issue I've been thinking about.
It's almost a shame that this only lives in UFL.
ufl/corealg/dag_traverser.py
Outdated
|
||
|
||
class DAGTraverser(ABC): | ||
"""Base class for dag traversers.""" |
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.
You should document the __init__
parameters here
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.
Done.
ufl/corealg/dag_traverser.py
Outdated
|
||
@staticmethod | ||
def postorder(method): | ||
"""Suppress processed operands in arguments. |
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.
Could this be clearer?
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.
I found it hard to keep it short. Please let me know if you have a suggestion. We at least have an expanded explanation right below.
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.
"Decorator indicating that child nodes are visited first in the traversal."?
I'm afraid I have no idea what the current docstring means.
It would also be fine to have something trivial like "Postorder decorator." provided that there is an explanation in the rest of the docstring.
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.
Changed to "Postorder decorator".
Processed object. | ||
""" | ||
raise AssertionError(f"UFL expression expected: got {o}") |
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.
Is this now boilerplate that needs reproducing for each subclass? Might it be better on the ABC?
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.
Do you mean putting this base case in the base class? I think the problem is that singledispatchmethod
object needs to be made in each child class. What I had to do here is probably relevant https://github.com/firedrakeproject/ufl/blob/c16f038378e17ad8bd730bf9a020028ad72a00c1/ufl/algorithms/apply_derivatives.py#L716.
|
||
def component_tensor(self, o, Ap, ii): | ||
@process.register(ComponentTensor) | ||
@DAGTraverser.postorder |
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 is pretty sweet
c16f038
to
449b44e
Compare
) | ||
|
||
@process.register(Derivative) | ||
def _(self, o: Expr) -> Expr: |
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.
Wouldn't it be better to use
@process.register
def _(self, o: Derivative) -> Expr:
here and in subsequent registrations, as https://docs.python.org/3/library/functools.html#functools.singledispatch states:
To add overloaded implementations to the function, use the register() attribute of the generic function, which can be used as a decorator. For functions annotated with types, the decorator will infer the type of the first argument automatically:
I guess we would have to wait for lower version Python to be 3.11 to do this for unions (python/cpython#30017). However, personally, I think this would be a cleaner solution. What do you think?
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.
I gave it a go with this as a separate commit, but I personally prefer:
@process.register(Grad)
@process.register(CellAvg)
@process.register(FacetAvg)
def _(self, o) -> Expr:
...
as type names are left-aligned and easier to read for me. I also do not quite see how it plays well when decorators are nested as in:
@process.register
@DAGTraverser.postorder
@pending_operations_recording
def external_operator(self, N: ExternalOperator, *dfs) -> Expr:
...
though functools.wraps()
seems to be doing the job for us.
I revert this commit for now as Python3.10 test fails as you already pointed out. Please see the commit history for the relevant changes.
ufl/corealg/dag_traverser.py
Outdated
""" | ||
|
||
def __init__(self, compress=True, visited_cache=None, result_cache=None) -> None: |
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.
def __init__(self, compress=True, visited_cache=None, result_cache=None) -> None: | |
def __init__(self, compress: bool=True, visited_cache=None, result_cache=None) -> None: |
Currently I do not think there is any test in ufl
that has a *args
as a non-empty argument. i.e. is always equal to args=()
. Could you add a test that has args
being non-empty?
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.
Should the type-hint for visited_cache
be dict[tuple[Expr, ...], Expr]|None
, and similarly for result_cache
?
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.
Added typehint.
That is right. It seems that we only need args = ()
in apply_derivatives.py
. We will need non-empty args
for instance when we want to pass down the state in apply_restrictions.py
; underneath a Restricted
node restricted='+'
or restricted='-'
, otherwise restricted=None
, where the method signature would look like:
@process.register(...)
def _(self, o, restricted):
...
We could remove *args
for this PR.
def coordinate_derivative(self, o, f, w, v, cd): | ||
@process.register(CoordinateDerivative) | ||
@DAGTraverser.postorder_only_children([0]) | ||
def _(self, o: Expr, f: Expr) -> Expr: |
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.
I guess a test on coordinate-derivative should yield an *args
that is non-empty.
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.
CoordinateDerivative
still only needs args=()
, I think.
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.
Yeah, I only looked at the signatures of the process method, but for the traversal of the graph, it seems like we never use args.
ufl/algorithms/apply_derivatives.py
Outdated
@@ -582,14 +710,35 @@ def min_value(self, o, df, dg): | |||
class GradRuleset(GenericDerivativeRuleset): | |||
"""Take the grad derivative.""" | |||
|
|||
def __init__(self, geometric_dimension): | |||
def __init__(self, geometric_dimension, compress=True, visited_cache=None, result_cache=None): |
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.
I would add type-hints here as well.
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.
Done.
@@ -1026,29 +1239,35 @@ class GateauxDerivativeRuleset(GenericDerivativeRuleset): | |||
D_w[v](e) = d/dtau e(w+tau v)|tau=0. | |||
""" | |||
|
|||
def __init__(self, coefficients, arguments, coefficient_derivatives): | |||
def __init__( |
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.
Type hints :)
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.
Done.
ufl/algorithms/apply_derivatives.py
Outdated
def _(self, o: Expr) -> Expr: | ||
return self._process_coefficient(o) | ||
|
||
def _process_coefficient(self, o: Expr) -> Expr: |
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.
Is the doc-string here strictly correct? (I know it existed prior to the PR).
As _process_coefficient
supports taking a BaseFormOperator
or a Cofunction
it is not really a Coefficient
.
I guess this goes back to the GateauxDerivativeRuleset
where any ExprList
can be taken as input and therefore might be out of scope of this PR.
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.
I partially fixed this (as I added these new methods), but I think we should do more comprehensive fix in a future PR.
In general very positive to this change. Will run the tests later today to check for potential regressions. |
Locally, i did not see a big regression with this PR, however, DOLFINx doesn't have the most complicated forms in the tests. |
Thanks for the ping. Quick testing on https://github.com/fenics-dolfiny/dolfiny/blob/main/demo/spectral/solid_elasticity_spectral.py - modified test case attached we see the following regression:
Modified example#!/usr/bin/env python3
import time
from mpi4py import MPI
from petsc4py import PETSc
import basix
import dolfinx
import ufl
from dolfinx import default_scalar_type as scalar
import mesh_tube3d_gmshapi as mg
import numpy as np
import plot_tube3d_pyvista as pl
import dolfiny
# Basic settings
name = "solid_elasticity_spectral"
comm = MPI.COMM_WORLD
# Geometry and mesh parameters
r, t, h = 0.04, 0.01, 0.1 # [m]
nr, nt, nh = 16, 5, 8
# Create the regular mesh of a tube with given dimensions
gmsh_model, tdim = mg.mesh_tube3d_gmshapi(name, r, t, h, nr, nt, nh, do_quads=True, order=2)
# Get mesh and meshtags
mesh, mts = dolfiny.mesh.gmsh_to_dolfin(gmsh_model, tdim)
# Get merged MeshTags for each codimension
subdomains, subdomains_keys = dolfiny.mesh.merge_meshtags(mesh, mts, tdim - 0)
interfaces, interfaces_keys = dolfiny.mesh.merge_meshtags(mesh, mts, tdim - 1)
# Define shorthands for labelled tags
surface_lower = interfaces_keys["surface_lower"]
surface_upper = interfaces_keys["surface_upper"]
# Define integration measures
dx = ufl.Measure("dx", domain=mesh, subdomain_data=subdomains, metadata={"quadrature_degree": 4})
ds = ufl.Measure("ds", domain=mesh, subdomain_data=interfaces, metadata={"quadrature_degree": 4})
# Define elements
Ue = basix.ufl.element("P", mesh.basix_cell(), 2, shape=(mesh.geometry.dim,))
# Define function spaces
Uf = dolfinx.fem.functionspace(mesh, Ue)
# Define functions
u = dolfinx.fem.Function(Uf, name="u")
u_ = dolfinx.fem.Function(Uf, name="u_") # boundary conditions
δu = ufl.TestFunction(Uf)
# Define state as (ordered) list of functions
m, δm = [u], [δu]
# output / visualisation
vorder = mesh.geometry.cmap.degree
uo = dolfinx.fem.Function(dolfinx.fem.functionspace(mesh, ("P", vorder, (3,))), name="u")
so = dolfinx.fem.Function(dolfinx.fem.functionspace(mesh, ("P", vorder)), name="s") # for output
# Kinematics
F = ufl.Identity(3) + ufl.grad(u)
# Strain measure: from Cauchy strain tensor to squares of principal stretches
c, _ = dolfiny.invariants.eigenstate(F.T * F) # spectral decomposition of C
c = ufl.as_vector(c) # squares of principal stretches
c = ufl.variable(c)
# Variation of strain measure (squares of principal stretches)
δc = dolfiny.expression.derivative(c, m, δm)
# Elasticity parameters
E = dolfinx.fem.Constant(mesh, scalar(1.0)) # [MPa]
nu = dolfinx.fem.Constant(mesh, scalar(0.4)) # [-]
mu = E / (2 * (1 + nu))
la = E * nu / ((1 + nu) * (1 - 2 * nu))
# Define boundary stress vector (torque at upper face)
x0 = ufl.SpatialCoordinate(mesh)
n0 = ufl.FacetNormal(mesh)
λ = dolfinx.fem.Constant(mesh, scalar(0.0))
t = ufl.cross(x0 - ufl.as_vector([0.0, 0.0, h]), n0) * 4 * λ # [N/m^2]
def strain_energy(i1, i2, i3):
"""Strain energy function
i1, i2, i3: principal invariants of the Cauchy-Green tensor
"""
# Determinant of configuration gradient F
J = ufl.sqrt(i3)
#
# Classical St. Venant-Kirchhoff
# Ψ = la / 8 * (i1 - 3) ** 2 + mu / 4 * ((i1 - 3) ** 2 + 4 * (i1 - 3) - 2 * (i2 - 3))
# Modified St. Venant-Kirchhoff
# Ψ = la / 2 * (ufl.ln(J)) ** 2 + mu / 4 * ((i1 - 3) ** 2 + 4 * (i1 - 3) - 2 * (i2 - 3))
# Compressible neo-Hooke
Ψ = mu / 2 * (i1 - 3 - 2 * ufl.ln(J)) + la / 2 * (J - 1) ** 2
# Compressible Mooney-Rivlin (beta = 0)
# Ψ = mu / 4 * (i1 - 3) + mu / 4 * (i2 - 3) - mu * ufl.ln(J) + la / 2 * (J - 1) ** 2
#
return Ψ
# Invariants (based on spectral decomposition of C)
i1, i2, i3 = c[0] + c[1] + c[2], c[0] * c[1] + c[1] * c[2] + c[0] * c[2], c[0] * c[1] * c[2]
# Material model (isotropic)
Ψ = strain_energy(i1, i2, i3)
# Stress measure
s = 2 * ufl.diff(Ψ, c)
# von Mises stress (output)
svm = ufl.sqrt(3 / 2 * ufl.inner(ufl.dev(ufl.diag(s)), ufl.dev(ufl.diag(s))))
# Weak form: for isotropic material, eigenprojectors of C and S are identical
form = -0.5 * ufl.inner(δc, s) * dx + ufl.inner(δu, t) * ds(surface_upper)
# Overall form (as list of forms)
forms = dolfiny.function.extract_blocks(form, δm)
form = forms[0]
n = 10
start = time.time()
for _ in range(n):
ufl.algorithms.compute_form_data(
form,
do_apply_function_pullbacks=True,
do_apply_default_restrictions=True,
do_apply_geometry_lowering=True,
do_apply_restrictions=True,
complex_mode=False,
)
print(f"F: {(time.time() - start)/n}s")
J_form = ufl.derivative(forms[0], m[0], ufl.TrialFunction(m[0].function_space))
start = time.time()
for _ in range(n):
ufl.algorithms.compute_form_data(
J_form,
do_apply_function_pullbacks=True,
do_apply_default_restrictions=True,
do_apply_geometry_lowering=True,
do_apply_restrictions=True,
complex_mode=False,
)
print(f"J: {(time.time() - start)/n}s") |
@schnellerhase your example is quite complex. import ufl
import time
from ufl import (
Coefficient,
FunctionSpace,
Mesh,
TestFunction,
hexahedron,
Constant,
)
from ufl.finiteelement import FiniteElement
from ufl.pullback import identity_pullback
from ufl.sobolevspace import H1
def invariants_principal(A):
"""Principal invariants of (real-valued) tensor A.
https://doi.org/10.1007/978-3-7091-0174-2_3
"""
i1 = ufl.tr(A)
i2 = (ufl.tr(A) ** 2 - ufl.tr(A * A)) / 2
i3 = ufl.det(A)
return i1, i2, i3
def eigenstate3(A):
"""Eigenvalues and eigenprojectors of the 3x3 (real-valued) tensor A.
Provides the spectral decomposition A = sum_{a=0}^{2} λ_a * E_a
with (ordered) eigenvalues λ_a and their associated eigenprojectors E_a = n_a^R x n_a^L.
Note: Tensor A must not have complex eigenvalues!
"""
if ufl.shape(A) != (3, 3):
raise RuntimeError(
f"Tensor A of shape {ufl.shape(A)} != (3, 3) is not supported!"
)
#
eps = 3.0e-16 # slightly above 2**-(53 - 1), see https://en.wikipedia.org/wiki/IEEE_754
#
A = ufl.variable(A)
#
# --- determine eigenvalues λ0, λ1, λ2
# Invariants
I1, _, _ = invariants_principal(A)
# Discriminant as sum-of-products
Δx = [
A[0, 1] * A[1, 2] * A[2, 0] - A[0, 2] * A[1, 0] * A[2, 1],
A[0, 1] ** 2 * A[1, 2]
- A[0, 1] * A[0, 2] * A[1, 1]
+ A[0, 1] * A[0, 2] * A[2, 2]
- A[0, 2] ** 2 * A[2, 1],
A[0, 0] * A[0, 1] * A[2, 1]
- A[0, 1] ** 2 * A[2, 0]
- A[0, 1] * A[2, 1] * A[2, 2]
+ A[0, 2] * A[2, 1] ** 2,
A[0, 0] * A[0, 2] * A[1, 2]
+ A[0, 1] * A[1, 2] ** 2
- A[0, 2] ** 2 * A[1, 0]
- A[0, 2] * A[1, 1] * A[1, 2],
A[0, 0] * A[0, 1] * A[1, 2]
- A[0, 1] * A[0, 2] * A[1, 0]
- A[0, 1] * A[1, 2] * A[2, 2]
+ A[0, 2] * A[1, 2] * A[2, 1],
A[0, 0] * A[0, 2] * A[2, 1]
- A[0, 1] * A[0, 2] * A[2, 0]
+ A[0, 1] * A[1, 2] * A[2, 1]
- A[0, 2] * A[1, 1] * A[2, 1],
A[0, 1] * A[1, 0] * A[1, 2]
- A[0, 2] * A[1, 0] * A[1, 1]
+ A[0, 2] * A[1, 0] * A[2, 2]
- A[0, 2] * A[1, 2] * A[2, 0],
A[0, 0] ** 2 * A[1, 2]
- A[0, 0] * A[0, 2] * A[1, 0]
- A[0, 0] * A[1, 1] * A[1, 2]
- A[0, 0] * A[1, 2] * A[2, 2]
+ A[0, 1] * A[1, 0] * A[1, 2]
+ A[0, 2] * A[1, 0] * A[2, 2]
+ A[1, 1] * A[1, 2] * A[2, 2]
- A[1, 2] ** 2 * A[2, 1],
A[0, 0] ** 2 * A[1, 2]
- A[0, 0] * A[0, 2] * A[1, 0]
- A[0, 0] * A[1, 1] * A[1, 2]
- A[0, 0] * A[1, 2] * A[2, 2]
+ A[0, 2] * A[1, 0] * A[1, 1]
+ A[0, 2] * A[1, 2] * A[2, 0]
+ A[1, 1] * A[1, 2] * A[2, 2]
- A[1, 2] ** 2 * A[2, 1],
A[0, 0] * A[0, 1] * A[1, 1]
- A[0, 0] * A[0, 1] * A[2, 2]
- A[0, 1] ** 2 * A[1, 0]
+ A[0, 1] * A[0, 2] * A[2, 0]
- A[0, 1] * A[1, 1] * A[2, 2]
+ A[0, 1] * A[2, 2] ** 2
+ A[0, 2] * A[1, 1] * A[2, 1]
- A[0, 2] * A[2, 1] * A[2, 2],
A[0, 0] * A[0, 1] * A[1, 1]
- A[0, 0] * A[0, 1] * A[2, 2]
+ A[0, 0] * A[0, 2] * A[2, 1]
- A[0, 1] ** 2 * A[1, 0]
- A[0, 1] * A[1, 1] * A[2, 2]
+ A[0, 1] * A[1, 2] * A[2, 1]
+ A[0, 1] * A[2, 2] ** 2
- A[0, 2] * A[2, 1] * A[2, 2],
A[0, 0] * A[0, 1] * A[1, 2]
- A[0, 0] * A[0, 2] * A[1, 1]
+ A[0, 0] * A[0, 2] * A[2, 2]
- A[0, 1] * A[1, 1] * A[1, 2]
- A[0, 2] ** 2 * A[2, 0]
+ A[0, 2] * A[1, 1] ** 2
- A[0, 2] * A[1, 1] * A[2, 2]
+ A[0, 2] * A[1, 2] * A[2, 1],
A[0, 0] * A[0, 2] * A[1, 1]
- A[0, 0] * A[0, 2] * A[2, 2]
- A[0, 1] * A[0, 2] * A[1, 0]
+ A[0, 1] * A[1, 1] * A[1, 2]
- A[0, 1] * A[1, 2] * A[2, 2]
+ A[0, 2] ** 2 * A[2, 0]
- A[0, 2] * A[1, 1] ** 2
+ A[0, 2] * A[1, 1] * A[2, 2],
A[0, 0] ** 2 * A[1, 1]
- A[0, 0] ** 2 * A[2, 2]
- A[0, 0] * A[0, 1] * A[1, 0]
+ A[0, 0] * A[0, 2] * A[2, 0]
- A[0, 0] * A[1, 1] ** 2
+ A[0, 0] * A[2, 2] ** 2
+ A[0, 1] * A[1, 0] * A[1, 1]
- A[0, 2] * A[2, 0] * A[2, 2]
+ A[1, 1] ** 2 * A[2, 2]
- A[1, 1] * A[1, 2] * A[2, 1]
- A[1, 1] * A[2, 2] ** 2
+ A[1, 2] * A[2, 1] * A[2, 2],
]
Δy = [
A[0, 2] * A[1, 0] * A[2, 1] - A[0, 1] * A[1, 2] * A[2, 0],
A[1, 0] ** 2 * A[2, 1]
- A[1, 0] * A[1, 1] * A[2, 0]
+ A[1, 0] * A[2, 0] * A[2, 2]
- A[1, 2] * A[2, 0] ** 2,
A[0, 0] * A[1, 0] * A[1, 2]
- A[0, 2] * A[1, 0] ** 2
- A[1, 0] * A[1, 2] * A[2, 2]
+ A[1, 2] ** 2 * A[2, 0],
A[0, 0] * A[2, 0] * A[2, 1]
- A[0, 1] * A[2, 0] ** 2
+ A[1, 0] * A[2, 1] ** 2
- A[1, 1] * A[2, 0] * A[2, 1],
A[0, 0] * A[1, 0] * A[2, 1]
- A[0, 1] * A[1, 0] * A[2, 0]
- A[1, 0] * A[2, 1] * A[2, 2]
+ A[1, 2] * A[2, 0] * A[2, 1],
A[0, 0] * A[1, 2] * A[2, 0]
- A[0, 2] * A[1, 0] * A[2, 0]
+ A[1, 0] * A[1, 2] * A[2, 1]
- A[1, 1] * A[1, 2] * A[2, 0],
A[0, 1] * A[1, 0] * A[2, 1]
- A[0, 1] * A[1, 1] * A[2, 0]
+ A[0, 1] * A[2, 0] * A[2, 2]
- A[0, 2] * A[2, 0] * A[2, 1],
A[0, 0] ** 2 * A[2, 1]
- A[0, 0] * A[0, 1] * A[2, 0]
- A[0, 0] * A[1, 1] * A[2, 1]
- A[0, 0] * A[2, 1] * A[2, 2]
+ A[0, 1] * A[1, 0] * A[2, 1]
+ A[0, 1] * A[2, 0] * A[2, 2]
+ A[1, 1] * A[2, 1] * A[2, 2]
- A[1, 2] * A[2, 1] ** 2,
A[0, 0] ** 2 * A[2, 1]
- A[0, 0] * A[0, 1] * A[2, 0]
- A[0, 0] * A[1, 1] * A[2, 1]
- A[0, 0] * A[2, 1] * A[2, 2]
+ A[0, 1] * A[1, 1] * A[2, 0]
+ A[0, 2] * A[2, 0] * A[2, 1]
+ A[1, 1] * A[2, 1] * A[2, 2]
- A[1, 2] * A[2, 1] ** 2,
A[0, 0] * A[1, 0] * A[1, 1]
- A[0, 0] * A[1, 0] * A[2, 2]
- A[0, 1] * A[1, 0] ** 2
+ A[0, 2] * A[1, 0] * A[2, 0]
- A[1, 0] * A[1, 1] * A[2, 2]
+ A[1, 0] * A[2, 2] ** 2
+ A[1, 1] * A[1, 2] * A[2, 0]
- A[1, 2] * A[2, 0] * A[2, 2],
A[0, 0] * A[1, 0] * A[1, 1]
- A[0, 0] * A[1, 0] * A[2, 2]
+ A[0, 0] * A[1, 2] * A[2, 0]
- A[0, 1] * A[1, 0] ** 2
- A[1, 0] * A[1, 1] * A[2, 2]
+ A[1, 0] * A[1, 2] * A[2, 1]
+ A[1, 0] * A[2, 2] ** 2
- A[1, 2] * A[2, 0] * A[2, 2],
A[0, 0] * A[1, 0] * A[2, 1]
- A[0, 0] * A[1, 1] * A[2, 0]
+ A[0, 0] * A[2, 0] * A[2, 2]
- A[0, 2] * A[2, 0] ** 2
- A[1, 0] * A[1, 1] * A[2, 1]
+ A[1, 1] ** 2 * A[2, 0]
- A[1, 1] * A[2, 0] * A[2, 2]
+ A[1, 2] * A[2, 0] * A[2, 1],
A[0, 0] * A[1, 1] * A[2, 0]
- A[0, 0] * A[2, 0] * A[2, 2]
- A[0, 1] * A[1, 0] * A[2, 0]
+ A[0, 2] * A[2, 0] ** 2
+ A[1, 0] * A[1, 1] * A[2, 1]
- A[1, 0] * A[2, 1] * A[2, 2]
- A[1, 1] ** 2 * A[2, 0]
+ A[1, 1] * A[2, 0] * A[2, 2],
A[0, 0] ** 2 * A[1, 1]
- A[0, 0] ** 2 * A[2, 2]
- A[0, 0] * A[0, 1] * A[1, 0]
+ A[0, 0] * A[0, 2] * A[2, 0]
- A[0, 0] * A[1, 1] ** 2
+ A[0, 0] * A[2, 2] ** 2
+ A[0, 1] * A[1, 0] * A[1, 1]
- A[0, 2] * A[2, 0] * A[2, 2]
+ A[1, 1] ** 2 * A[2, 2]
- A[1, 1] * A[1, 2] * A[2, 1]
- A[1, 1] * A[2, 2] ** 2
+ A[1, 2] * A[2, 1] * A[2, 2],
]
Δd = [9, 6, 6, 6, 8, 8, 8, 2, 2, 2, 2, 2, 2, 1]
Δ = sum(Δxk * Δdk * Δyk for Δxk, Δdk, Δyk in zip(Δx, Δd, Δy)) # discriminant as sop
# Invariant dp as sum-of-products
Δxp = [
A[1, 0],
A[2, 0],
A[2, 1],
-A[0, 0] + A[1, 1],
-A[0, 0] + A[2, 2],
-A[1, 1] + A[2, 2],
]
Δyp = [
A[0, 1],
A[0, 2],
A[1, 2],
-A[0, 0] + A[1, 1],
-A[0, 0] + A[2, 2],
-A[1, 1] + A[2, 2],
]
Δdp = [6, 6, 6, 1, 1, 1]
dp = (
sum(Δxpk * Δdpk * Δypk for Δxpk, Δdpk, Δypk in zip(Δxp, Δdp, Δyp)) / 2
) # dp as sop
# Invariant dq as sum-of-products
Δxq = [
A[1, 2],
A[2, 1],
A[0, 1] * A[1, 0],
A[0, 2] * A[2, 0],
A[1, 2] * A[2, 1],
A[1, 1] + A[2, 2] - 2 * A[0, 0],
]
Δyq = [
A[0, 1] * A[2, 0],
A[0, 2] * A[1, 0],
A[0, 0] + A[1, 1] - 2 * A[2, 2],
A[0, 0] + A[2, 2] - 2 * A[1, 1],
A[1, 1] + A[2, 2] - 2 * A[0, 0],
(A[0, 0] + A[2, 2] - 2 * A[1, 1]) * (A[0, 0] + A[1, 1] - 2 * A[2, 2]),
]
Δdq = [27, 27, 9, 9, 9, -1]
dq = sum(Δxqk * Δdqk * Δyqk for Δxqk, Δdqk, Δyqk in zip(Δxq, Δdq, Δyq))
# Avoid dp = 0 and disc = 0, both are known with absolute error of ~eps**2
# Required to avoid sqrt(0) derivatives and negative square roots
dp += eps**2
Δ += eps**2
phi3 = ufl.atan2(ufl.sqrt(27) * ufl.sqrt(Δ), dq)
# sorted eigenvalues: λ0 <= λ1 <= λ2
λ = [
(I1 + 2 * ufl.sqrt(dp) * ufl.cos((phi3 + 2 * ufl.pi * k) / 3)) / 3
for k in range(1, 4)
]
#
# --- determine eigenprojectors E0, E1, E2
#
E = [ufl.diff(λk, A).T for λk in λ]
return λ, E
def eigenstate2(A):
"""Eigenvalues and eigenprojectors of the 2x2 (real-valued) tensor A.
Provides the spectral decomposition A = sum_{a=0}^{1} λ_a * E_a
with (ordered) eigenvalues λ_a and their associated eigenprojectors E_a = n_a^R x n_a^L.
Note: Tensor A must not have complex eigenvalues!
"""
if ufl.shape(A) != (2, 2):
raise RuntimeError(
f"Tensor A of shape {ufl.shape(A)} != (2, 2) is not supported!"
)
#
eps = 3.0e-16 # slightly above 2**-(53 - 1), see https://en.wikipedia.org/wiki/IEEE_754
#
A = ufl.variable(A)
#
# --- determine eigenvalues λ0, λ1
#
I1, _, _ = invariants_principal(A)
#
Δ = (A[0, 0] - A[1, 1]) ** 2 + 4 * A[0, 1] * A[1, 0] # = I1**2 - 4 * I2
# Avoid dp = 0 and disc = 0, both are known with absolute error of ~eps**2
# Required to avoid sqrt(0) derivatives and negative square roots
Δ += eps**2
# sorted eigenvalues: λ0 <= λ1
λ = (I1 - ufl.sqrt(Δ)) / 2, (I1 + ufl.sqrt(Δ)) / 2
#
# --- determine eigenprojectors E0, E1
#
E = [ufl.diff(λk, A).T for λk in λ]
return λ, E
def eigenstate(A):
"""Eigenvalues and eigenprojectors of the (real-valued) tensor A of dimension m = 2, 3.
Provides the spectral decomposition A = sum_{a=0}^{m} λ_a * E_a
with (ordered) eigenvalues λ_a and their associated eigenprojectors E_a = n_a^R x n_a^L.
Note: Tensor A must not have complex eigenvalues!
"""
if ufl.shape(A) == (3, 3):
return eigenstate3(A)
elif ufl.shape(A) == (2, 2):
return eigenstate2(A)
else:
raise RuntimeError(f"Tensor A of shape {ufl.shape(A)} is not supported!")
element = FiniteElement("Lagrange", hexahedron, 2, (3,), identity_pullback, H1)
domain = Mesh(FiniteElement("Lagrange", hexahedron, 2, (3,), identity_pullback, H1))
# Define integration measures
dx = ufl.Measure("dx", domain=domain, metadata={"quadrature_degree": 4})
ds = ufl.Measure("ds", domain=domain, metadata={"quadrature_degree": 4})
Uf = FunctionSpace(domain, element)
u = Coefficient(Uf) # displacement
u_ = Coefficient(Uf)
δu = TestFunction(Uf)
# output / visualisation
# Kinematics
F = ufl.Identity(3) + ufl.grad(u)
# Strain measure: from Cauchy strain tensor to squares of principal stretches
c, _ = eigenstate(F.T * F) # spectral decomposition of C
c = ufl.as_vector(c) # squares of principal stretches
c = ufl.variable(c)
# Variation of strain measure (squares of principal stretches)
δc = ufl.derivative(c, u, δu)
# Elasticity parameters
E = Constant(domain) # [MPa]
nu = Constant(domain)
mu = E / (2 * (1 + nu))
la = E * nu / ((1 + nu) * (1 - 2 * nu))
# Define boundary stress vector (torque at upper face)
x0 = ufl.SpatialCoordinate(domain)
n0 = ufl.FacetNormal(domain)
λ = Constant(domain)
h = Constant(domain) # [m]
t = ufl.cross(x0 - ufl.as_vector([0.0, 0.0, h]), n0) * 4 * λ # [N/m^2]
def strain_energy(i1, i2, i3):
"""Strain energy function
i1, i2, i3: principal invariants of the Cauchy-Green tensor
"""
# Determinant of configuration gradient F
J = ufl.sqrt(i3)
#
# Classical St. Venant-Kirchhoff
# Ψ = la / 8 * (i1 - 3) ** 2 + mu / 4 * ((i1 - 3) ** 2 + 4 * (i1 - 3) - 2 * (i2 - 3))
# Modified St. Venant-Kirchhoff
# Ψ = la / 2 * (ufl.ln(J)) ** 2 + mu / 4 * ((i1 - 3) ** 2 + 4 * (i1 - 3) - 2 * (i2 - 3))
# Compressible neo-Hooke
Ψ = mu / 2 * (i1 - 3 - 2 * ufl.ln(J)) + la / 2 * (J - 1) ** 2
# Compressible Mooney-Rivlin (beta = 0)
# Ψ = mu / 4 * (i1 - 3) + mu / 4 * (i2 - 3) - mu * ufl.ln(J) + la / 2 * (J - 1) ** 2
#
return Ψ
# Invariants (based on spectral decomposition of C)
i1, i2, i3 = (
c[0] + c[1] + c[2],
c[0] * c[1] + c[1] * c[2] + c[0] * c[2],
c[0] * c[1] * c[2],
)
# Material model (isotropic)
Ψ = strain_energy(i1, i2, i3)
# Stress measure
s = 2 * ufl.diff(Ψ, c)
# von Mises stress (output)
svm = ufl.sqrt(3 / 2 * ufl.inner(ufl.dev(ufl.diag(s)), ufl.dev(ufl.diag(s))))
# Weak form: for isotropic material, eigenprojectors of C and S are identical
form = -0.5 * ufl.inner(δc, s) * dx + ufl.inner(δu, t) * ds(1)
start = time.perf_counter()
ufl.algorithms.compute_form_data(
form,
do_apply_function_pullbacks=True,
do_apply_default_restrictions=True,
do_apply_geometry_lowering=True,
do_apply_restrictions=True,
complex_mode=False,
)
end = time.perf_counter()
print(f"Time taken to compute form data (residual): {(end - start):.2e}")
J = ufl.derivative(form, u)
start = time.perf_counter()
ufl.algorithms.compute_form_data(
J,
do_apply_function_pullbacks=True,
do_apply_default_restrictions=True,
do_apply_geometry_lowering=True,
do_apply_restrictions=True,
complex_mode=False,
)
end = time.perf_counter()
print(f"Time taken to compute form data (jacobian): {(end - start):.2e}") which results in the following timings on main: Time taken to compute form data (residual): 1.11e-01
Time taken to compute form data (jacobian): 2.14e-01 and the following on this branch: Time taken to compute form data (residual): 4.97e-01
Time taken to compute form data (jacobian): 3.65e+00 |
There was a small typo in the example above (didn't compile form data for jacobian). |
Thanks! I had not updated my branch after some performance fix was merged to
Let me also update the results for the other tests in the PR description:
|
a0442e7
to
8ecb3a3
Compare
- replace MultiFunction with DAGTraverser in apply_derivatives.py Co-authored-by: Connor Ward <[email protected]> Co-authored-by: Jørgen Schartum Dokken <[email protected]>
3d63435
to
a111e2e
Compare
This reverts commit 25e2a49.
Attempt to implement what @wence- described in this issue.
We basically would like to introduce classes (
DAGTraverser
s) that define node processing (usingsingledispatchmethod
) and hold caches.We considered two DAG traversal approaches (for post-order and mostly post-order traversals).
Approach 1. Two-step approach.
Collect nodes first post-order and then process them in order.
This is the current UFL approach (
cutoff_unique_post_traversal
+MultiFunction
wrapped inmap_expr_dags
).This "bottom-up" approach is optimal, but when we process a node, we no longer know the local DAG structure around that node (e.g., we do not know its parent). So if we want to pass down some state from the parent, we need to (indirectly) specify that that parent is a "cutoff" node (so that we do not collect the child nodes in the first step), and perform a sub-DAG traversal under that parent; see RestrictionPropagator.restricted(self, o). In the current implementation, if the number of arguments to the method is two (
self
ando
in the above case), the corresponding node type is regarded as a cutoff node type.MultiFunction
constructor currently identifies cutoff node types byinspect
ing the signature of each method. If we wanted to do a similar thing withsingledispatchmethod
, we could subclasssingledispatchmethod
as David suggested, but we found that we would end up overwritingsingledispatchmethod.register()
method relying on the current implementation ofsingledispatchmethod
, which would be a drawback.Approach 2. Monolithic approach.
Use recursion with caching (each node is processed once and only once) described in this issue.
I observed about a few % - 20% overhead after rewriting
apply_derivatives.py
(please see below) presumably due to recursion, but no special consideration of cutoff is required. This approach is claimed to be more robust.In this PR we take Approach2, and replace
MultiFunction
s withDAGTraverser
s inapply_derivatives.py
. We should be able to incrementally/systematically remove allMultiFunction
s in the future.Performance checks:
holzapfel_ogden.py
(holzapfel_ogden):main
:average time required: 0.27037227153778076
this PR
:average time required: 0.27785348892211914 (+2.8%)
With #339 (<- performance regression) reverted:
main
:average time required: 0.04610729217529297
this PR
:average time required: 0.055258870124816895 (+20%)
wence_test
(#69):main
:average time required: 0.23360061645507812
this PR
:average time required: 0.2485201358795166 (+6.4%)
With #339 reverted:
main
:average time required: 0.060405850410461426
this PR
:average time required: 0.06431174278259277 (+6.5%)
Firedrake CI
firedrakeproject/firedrake#4145 (no notable performance regression).