-
-
Notifications
You must be signed in to change notification settings - Fork 67
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
introduce MeshSequence class #303
base: main
Are you sure you want to change the base?
introduce MeshSequence class #303
Conversation
0eeb02f
to
71d79f4
Compare
ufl/algorithms/analysis.py
Outdated
base_coeff_and_args = extract_type(a, (BaseArgument, BaseCoefficient)) | ||
arguments = [f for f in base_coeff_and_args if isinstance(f, BaseArgument)] | ||
coefficients = [f for f in base_coeff_and_args if isinstance(f, BaseCoefficient)] | ||
base_coeff_and_args_and_gq = extract_type(a, (BaseArgument, BaseCoefficient, GeometricQuantity)) |
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 we just call this list base_types
, as it is quite explicit from the function call what it does, and it makes the latter code more readable.
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.
Renamed it terminals
.
domain = MixedMesh(mesh0, mesh1, mesh2) | ||
V = FunctionSpace(domain, elem) |
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.
So we now have two ways of creating a FunctionSpace
with MixedElement
s.
- A single mesh of class
Mesh
with aMixedElement
, whereMixedElement
can have arbitrary many elements - A
MixedMesh
consisting ofM
meshes, and aMixedElement
consisting ofM
elements.
Do we need additional checks in the FunctionSpace
constructor to ensure that this is satisfied (instead of catching this at a later instance, inside for instance GenericDerivativeRuleset.reference_value
.
I would also like to highlight that the API for MixedElement
and MixedMeshes
differs, as one passes a list, while the other unrolls a list, I think we should be consistent, and use lists in both places
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 checks in FunctionSpace
constructor and changed MixedMesh
API as suggested. Thanks.
ufl/algorithms/apply_derivatives.py
Outdated
if element.num_sub_elements != len(domain): | ||
raise RuntimeError(f"{element.num_sub_elements} != {len(domain)}") | ||
g = ReferenceGrad(o) | ||
vsh = g.ufl_shape[:-1] |
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 we give this a more obvious name?
Isn't this isn't this just the ufl_shape
of the input operand, which should always be the same as the ufl_shape
of o
.
Another question (as I don't quite see how this works) is what is the ref_dim
?, which would be the topological dimension of the unique "ufl-domain" of o
. But what is the topological dimension of a MixedMesh
?
Does this mean that a MixedMesh
cannot have co-dimension 1 meshes in them, i.e. a Mesh of triangles and a Mesh of intervals?
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.
Turned out vsh
could be removed, so I did.
You are right. Currently, MixedMesh
can only be composed of meshes of the same cell_type, so topological dimension is well defined. I edited the doc string of MixedMesh
to make this point clear.
It would be helpful to have a PR summary, e.g. what it sets out the achieve, what it supports, designs considered, design rationale, implementation summary, and what a MixedMesh is and how a MixedMesh is different from a Mesh. |
6dbbd72
to
3512b14
Compare
Updated PR summary. Added more checks. Fixed |
3512b14
to
510af2f
Compare
Can I have another round of review on this? |
510af2f
to
9c1e697
Compare
355887f
to
1614420
Compare
ufl/algorithms/apply_derivatives.py
Outdated
def flatten_domain_element(domain, element): | ||
"""Return the flattened (domain, element) pairs for mixed domain problems.""" | ||
if not isinstance(domain, MixedMesh): | ||
return ((domain, element),) | ||
flattened = () | ||
assert len(domain) == len(element.sub_elements) | ||
for d, e in zip(domain, element.sub_elements): | ||
flattened += flatten_domain_element(d, e) | ||
return flattened |
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.
Typing + specification of what is returned if the problem is not a "mixed domain problem"/
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
for d, e in flatten_domain_element(domain, element): | ||
esh = e.reference_value_shape | ||
if esh == (): | ||
esh = (1,) | ||
if isinstance(e.pullback, PhysicalPullback): | ||
if ref_dim != self._var_shape[0]: | ||
raise NotImplementedError(""" | ||
PhysicalPullback not handled for immersed domain : | ||
reference dim ({ref_dim}) != physical dim (self._var_shape[0])""") | ||
for idx in range(int(np.prod(esh))): | ||
for i in range(ref_dim): | ||
components.append(g[(dofoffset + idx,) + (i,)]) | ||
else: | ||
K = JacobianInverse(d) | ||
rdim, gdim = K.ufl_shape | ||
if rdim != ref_dim: | ||
raise RuntimeError(f"{rdim} != {ref_dim}") | ||
if gdim != self._var_shape[0]: | ||
raise RuntimeError(f"{gdim} != {self._var_shape[0]}") | ||
for idx in range(int(np.prod(esh))): | ||
for i in range(gdim): | ||
temp = Zero() | ||
for j in range(rdim): | ||
temp += g[(dofoffset + idx,) + (j,)] * K[j, i] | ||
components.append(temp) | ||
dofoffset += int(np.prod(esh)) | ||
return as_tensor(np.asarray(components).reshape(g.ufl_shape[:-1] + self._var_shape)) |
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.
Does one need the conditional when flatten_domain_element
works for non-mixed meshes?
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.
Shape of g
(which I now call rgrad
) is slightly different for scalar and non-scalar spaces, e.g. (3,)
v.s. (5, 3)
for topological dim = 3
, so g[(dofoffset + idx,) + (j,)]
fails for scalar spaces. We could special case for the scalar space inside the flatten_domain_element
loop, but I thought it would be cleaner if we just did that outside.
ufl/algorithms/apply_derivatives.py
Outdated
for d, e in flatten_domain_element(domain, element): | ||
esh = e.reference_value_shape | ||
if esh == (): | ||
esh = (1,) | ||
if isinstance(e.pullback, PhysicalPullback): | ||
if ref_dim != self._var_shape[0]: | ||
raise NotImplementedError(""" | ||
PhysicalPullback not handled for immersed domain : | ||
reference dim ({ref_dim}) != physical dim (self._var_shape[0])""") | ||
for idx in range(int(np.prod(esh))): | ||
for i in range(ref_dim): | ||
components.append(g[(dofoffset + idx,) + (i,)]) | ||
else: | ||
K = JacobianInverse(d) | ||
rdim, gdim = K.ufl_shape | ||
if rdim != ref_dim: | ||
raise RuntimeError(f"{rdim} != {ref_dim}") | ||
if gdim != self._var_shape[0]: | ||
raise RuntimeError(f"{gdim} != {self._var_shape[0]}") | ||
for idx in range(int(np.prod(esh))): | ||
for i in range(gdim): | ||
temp = Zero() | ||
for j in range(rdim): | ||
temp += g[(dofoffset + idx,) + (j,)] * K[j, i] | ||
components.append(temp) | ||
dofoffset += int(np.prod(esh)) |
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.
Some documentation of the logic within this loop would be nice.
I guess the idea is:
- For a mixed domain problem, accumulate all derivatives in expanded index notation.
Please correct me if I am mistaken.
I think one thing that would help is to define np.prod(esh)
as a variable, as it is used in multiple loops and to accumulate the counter.
Also, I think you do not need the
if esh == ():
esh = (1,)
as int(np.prod(()))
ref: https://numpy.org/doc/2.1/reference/generated/numpy.prod.html
The product of an empty array is the neutral element 1
One could add a guard against future changing behavior by adding
esh = e.reference_value_shape
num_components = int(np.prod(esh))
assert num_components > 0
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 are right. Simplified as suggested and added more comments. Thanks.
ufl/algorithms/apply_derivatives.py
Outdated
temp += g[(dofoffset + idx,) + (j,)] * K[j, i] | ||
components.append(temp) | ||
dofoffset += int(np.prod(esh)) | ||
return as_tensor(np.asarray(components).reshape(g.ufl_shape[:-1] + self._var_shape)) |
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 using numpy.asarray
safe 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.
I think so.
ufl/algorithms/apply_derivatives.py
Outdated
element = f.ufl_function_space().ufl_element() | ||
if element.num_sub_elements != len(domain): | ||
raise RuntimeError(f"{element.num_sub_elements} != {len(domain)}") | ||
g = ReferenceGrad(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.
Same comment as for previous function
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.
Made similar changes here, too.
ufl/algorithms/compute_form_data.py
Outdated
all_domains = extract_domains(integral.integrand(), expand_mixed_mesh=False) | ||
if any(isinstance(m, MixedMesh) for m in all_domains): | ||
raise NotImplementedError(""" | ||
Only integral_type="cell" can be used with MixedMesh""") |
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.
Maybe add what cell type you got as input to error message.
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.
Elaborated the error message.
ufl/domain.py
Outdated
"""Return the component meshes.""" | ||
return (self,) | ||
|
||
def iterable_like(self, 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.
Typing-hinting for new functions in UFL would be nice.
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 should be type-hinted as it has assumptions on the fact that element has the property sub_elements
.
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.
Why not use num_sub_elments
as in mixedmesh
?
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 type-hinting. Right, we now use num_sub_elements
.
ufl/domain.py
Outdated
|
||
""" | ||
|
||
def __init__(self, meshes, ufl_id=None, cargo=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.
Type-hinting everything but cargo
(as that is implementation specific).
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/domain.py
Outdated
element.num_sub_elements ({element.num_sub_elements})""") | ||
return self | ||
|
||
def can_make_function_space(self, 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.
Type-hinting.
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/domain.py
Outdated
|
||
|
||
def sort_domains(domains): | ||
"""Sort domains in a canonical ordering.""" | ||
return tuple(sorted(domains, key=lambda domain: domain._ufl_sort_key_())) | ||
|
||
|
||
def join_domains(domains): | ||
def join_domains(domains, expand_mixed_mesh=True): |
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.
Add comment about what expand mixed mesh true/false means + type-hinting.
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/domain.py
Outdated
@@ -226,20 +374,26 @@ def join_domains(domains): | |||
# TODO: Move these to an analysis module? | |||
|
|||
|
|||
def extract_domains(expr): | |||
def extract_domains(expr, expand_mixed_mesh=True): |
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.
Add documentation regarding keyword argument expand_mixed_mesh
.
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.
Copied from Slack: The implementation looks OK to me (quick look), but the tests look a bit light. In terms of the naming, I was also confused by the use of Mixed. At least when we’re talking about MixedElement, aside from it having historical meaning, my impression was that we are talking about using the Cartesian product on the elements of the list and maintaining ordering. My understanding of MixedMesh is that we are maintaining the ordering, but I didn’t get the impression it was a Cartesian product - consequently, the use of Mixed wasn’t clear to me. This is aside from the idea that Mixed Mesh has no traditional meaning in the literature - when the idea was first proposed I thought it was related to having a mesh with mixed cell type, to illustrate the confusion. Of course these are ‘misconceptions’ on my part, but I’m just trying to get across the confusion that appeared in my own mind, so we can discuss if different naming of the same concept might be appropriate. |
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 am happy to be argued down but personally I think the abstraction is a bit strange. Why can we not have, instead of
cell = triangle
elem0 = FiniteElement("Lagrange", cell, ...)
elem1 = FiniteElement("Brezzi-Douglas-Marini", cell, ...)
elem2 = FiniteElement("Discontinuous Lagrange", cell, ...)
elem = MixedElement([elem0, elem1, elem2])
mesh0 = Mesh(FiniteElement("Lagrange", cell, ...))
mesh1 = Mesh(FiniteElement("Lagrange", cell, ...))
mesh2 = Mesh(FiniteElement("Lagrange", cell, ...))
domain = MixedMesh([mesh0, mesh1, mesh2])
V = FunctionSpace(domain, elem)
something like
cell = triangle
elem0 = FiniteElement("Lagrange", cell, ...)
mesh0 = Mesh(FiniteElement("Lagrange", cell, ...))
V0 = FunctionSpace(mesh0, elem0)
elem1 = FiniteElement("Brezzi-Douglas-Marini", cell, ...)
mesh1 = Mesh(FiniteElement("Lagrange", cell, ...))
V1 = FunctionSpace(mesh1, elem1)
elem2 = FiniteElement("Discontinuous Lagrange", cell, ...)
mesh2 = Mesh(FiniteElement("Lagrange", cell, ...))
V2 = FunctionSpace(mesh2, elem2)
V = MixedFunctionSpace([V0, V1, V2])
where we have a MixedFunctionSpace
instead of a MixedMesh
.
More generally what is the difference between a FunctionSpace
with a MixedElement
and a MixedFunctionSpace
using non-Mixed
elements?
This is what we use in DOLFINx at the moment. I think the current mixed-mesh abstraction is going to be confusing when we introduce meshes with multiple cell types (ie mix of quadrilateral and triangle, or hex, yet, prism). |
My plan is to introduce We could have: single mesh + single space, single mesh + multiple spaces, and multiple meshes + multiple spaces. I think the problem of The
MixedMesh` might not be very intuitive, but I think it is the right abstraction. |
If the only thing that makes On the other hand, if everyone agrees that
When you say multiple meshes + multiple spaces, do you mean: V = FunctionSpace(mesh, el0)
submesh = SubMesh(mesh, ...)
mixed_element = MixedElement([el0, el1])
Q = FunctionSpace(submesh, mixed_element)
W = MixedFunctionSpace(V, Q) equivalent to: elem0 = FiniteElement("Lagrange", cell, ...)
elem1 = FiniteElement("Brezzi-Douglas-Marini", cell, ...)
mixed_element = MixedElement([el0, el1])
elem = MixedElement([elem0, mixed_element])
mesh0 = Mesh(FiniteElement("Lagrange", cell, ...))
mesh1 = Mesh(FiniteElement("Lagrange", cell, ...))
domain = MixedMesh([mesh0, mesh1])
V = FunctionSpace(domain, elem) or are you meaning something different?
I think we need a better name for this than "mixed", as a FunctionSpace of MixedElement on a MixedMesh with a "sub"-mesh that has a MixedCell sounds incredibly cryptic (which would be something like solve stokes equation (u, p) pair on a subdomain in your mesh, which consists of triangle and quadrilateral cells, coupled with a diffusivity coefficient living on the whole domain). |
How would this compose with a mesh which didn't have a unique cell type (because it contains triangles and quads, for example)? |
I think we should separate the name from the object. E.g. we could call it a I think the object disagreement here is caused by a long-standing difference of viewpoint about So, you can make MixedFunctionSpace a separate class, which has always been the FEniCS approach (at which point, why have a MixedElement class, you might ask) or you can use a unified FunctionSpace and push the mixed information down to the element, which has always been the Firedrake approach. In the latter case, once you have multiple domains you find yourself wanting a way of reasoning about them, which is facilitated by the class encompassing the meshes. Basically, once you have mixed there are different levels in the dependency graph at which you can think about the objects coming together. I think this might be a case where we should just allow for both in order to let Firedrake and FEniCSx respectively just get on with it. |
Actually, if we follow the pattern, then the elements on a mixed topology domain are themselves a sort of mixed element. No problem. |
@jorgensd I do not think merely allowing for I did not mean nested mixed spaces by "multiple meshes + multiple spaces". I just meant (meshA, elemA) x (meshB, elemB). Though we do not use nested mixed spaces in Firedrake, some functions such as @jhale I think the use of But I agree that it is confusing. As @dham suggested, |
Let's not use |
@ksagiyam I see your analogy between |
2073e67
to
bf1fcff
Compare
Thanks, all. Yes, let us use
@jhale What sort of tests do you think are missing? After this PR, I will soon make another PR that adds logics to handle restrictions (to handle cases where exterior facet integration of meshA is interior facet integration of meshB, etc). At that stage I will add one or two more examples that test facet integrations. |
b1f293d
to
3fe26ca
Compare
Co-authored-by: Jørgen Schartum Dokken <[email protected]>
3fe26ca
to
df25c20
Compare
First half of #264
Introduce
MixedMesh
class for multi-mesh problems.Note that
extract_domains()
now uses more robustsort_domains()
inside, so it might behave slightly differently.Edit 12-09-2024:
MixedMesh
class represents a collection of meshes (e.g., submeshes) that, along with aMixedElement
, can represent a mixed function space defined across multiple domains.The motivation is to allow for treating
Argument
s andCoefficient
s on a mixed function space defined across multiple domains just like those on a mixed function space defined on a single mesh.Specifically, the following becomes possible (see tests for more):
For now, one can only perform cell integrations when
MixedMesh
es are used. This is because, e.g., an interior facet integration on a submesh may either be interior or exterior facet integration on the parent mesh, and we need a logic to apply default restrictions on coefficients defined on the participating meshes. This is the second half of #264.Also, currently, all component meshes must have the same cell type (and thus the same topological dimension) -- we are to remove this limitation in the future.
Core changes:
GradRuleSet.{reference_value, reference_grad}
work component-wise (component of the mixed space) if theFunctionSpace
is defined on aMixedMesh
, so that each component is associated with a component of theMixedMesh
, saydomain_i
(JacobianInverse(domain_i)
is then well defined).extract_arguments_and_coefficients
is now calledextract_terminals_with_domain
, and it now also collectsGeometricQuanty
s, so that we can correctly handle, saySpatialCoordinate(mesh1)
andSpatialCoordinate(mesh2)
, in problem solving environments.