Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

ksagiyam
Copy link
Contributor

@ksagiyam ksagiyam commented Aug 21, 2024

First half of #264

Introduce MixedMesh class for multi-mesh problems.

Note that extract_domains() now uses more robust sort_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 a MixedElement, can represent a mixed function space defined across multiple domains.
The motivation is to allow for treating Arguments and Coefficients 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):

    cell = triangle
    mesh0 = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1))
    mesh1 = Mesh(FiniteElement("Lagrange", cell, 1, (2,), identity_pullback, H1))
    domain = MixedMesh([mesh0, mesh1])
    elem0 = FiniteElement("Lagrange", cell, 1, (), identity_pullback, H1)
    elem1 = FiniteElement("Lagrange", cell, 2, (), identity_pullback, H1)
    elem = MixedElement([elem0, elem1])
    V = FunctionSpace(domain, elem)
    v = TestFunction(V)
    v0, v1 = split(v)

For now, one can only perform cell integrations when MixedMeshes 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 the FunctionSpace is defined on a MixedMesh, so that each component is associated with a component of the MixedMesh, say domain_i (JacobianInverse(domain_i) is then well defined).
  • extract_arguments_and_coefficients is now called extract_terminals_with_domain, and it now also collects GeometricQuantys, so that we can correctly handle, say SpatialCoordinate(mesh1) and SpatialCoordinate(mesh2), in problem solving environments.

ufl/domain.py Outdated Show resolved Hide resolved
@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch from 0eeb02f to 71d79f4 Compare August 21, 2024 13:16
@ksagiyam
Copy link
Contributor Author

ksagiyam commented Sep 6, 2024

@jpdean @jorgensd This is what we briefly discussed at PDESoft. Could you have a look?

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))
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it terminals.

Comment on lines 34 to 35
domain = MixedMesh(mesh0, mesh1, mesh2)
V = FunctionSpace(domain, elem)
Copy link
Member

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 MixedElements.

  1. A single mesh of class Mesh with a MixedElement, where MixedElement can have arbitrary many elements
  2. A MixedMesh consisting of M meshes, and a MixedElement consisting of M 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

Copy link
Contributor Author

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.

if element.num_sub_elements != len(domain):
raise RuntimeError(f"{element.num_sub_elements} != {len(domain)}")
g = ReferenceGrad(o)
vsh = g.ufl_shape[:-1]
Copy link
Member

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?

Copy link
Contributor Author

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.

@garth-wells
Copy link
Member

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.

@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch from 6dbbd72 to 3512b14 Compare September 12, 2024 12:46
@ksagiyam
Copy link
Contributor Author

Updated PR summary.

Added more checks.

Fixed MixedPullback.physical_value_shape().

@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch from 3512b14 to 510af2f Compare October 15, 2024 14:03
@ksagiyam
Copy link
Contributor Author

Can I have another round of review on this?

@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch from 510af2f to 9c1e697 Compare November 15, 2024 22:11
@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch 2 times, most recently from 355887f to 1614420 Compare December 19, 2024 12:38
Comment on lines 89 to 106
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
Copy link
Member

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"/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 684 to 710
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))
Copy link
Member

@jorgensd jorgensd Jan 6, 2025

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?

Copy link
Contributor Author

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.

Comment on lines 684 to 709
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))
Copy link
Member

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

Copy link
Contributor Author

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.

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))
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so.

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)
Copy link
Member

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

Copy link
Contributor Author

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.

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""")
Copy link
Member

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
ufl/domain.py Outdated
"""Return the component meshes."""
return (self,)

def iterable_like(self, element):
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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?

Copy link
Contributor Author

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):
Copy link
Member

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

ufl/domain.py Show resolved Hide resolved
ufl/domain.py Outdated
element.num_sub_elements ({element.num_sub_elements})""")
return self

def can_make_function_space(self, element):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type-hinting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

ufl/domain.py Show resolved Hide resolved
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):
Copy link
Member

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.

Copy link
Contributor Author

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):
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

ufl/domain.py Show resolved Hide resolved
@jhale
Copy link
Member

jhale commented Jan 6, 2025

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.

Copy link
Contributor

@connorjward connorjward left a 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?

@jorgensd
Copy link
Member

jorgensd commented Jan 7, 2025

    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, ...)
  

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).

@ksagiyam
Copy link
Contributor Author

ksagiyam commented Jan 7, 2025

My plan is to introduce MixedCell that should be shared by the MixedMesh and the MixedElement for mixed problems with multiple cell types.

We could have: single mesh + single space, single mesh + multiple spaces, and multiple meshes + multiple spaces.
Current MixedFunctionSpace approach handles the last case differently inside UFL (and inside DOLFINx, I believe).
Simply put, MixedMesh allows us to handle the last case just like the first two cases in UFL.

I think the problem of MixedFunctionSpace is that it behaves differently from the standard FunctionSpace, and requires special casing at high-level. For instance, we can not define Coefficient on a MixedFunctionSpace; we need to use the (special-cased) Coefficients function, which returns a list of Coefficients. We thus can not use high-level functions, such as inner and split, for coefficients on MixedFunctionSpaces.
Components of an Argument should also be consistently represented by Indexed(argument, index) in UFL instead of by part, I think.

The MixedMesh approach keeps the set of basic building blocks, such as Mesh, FunctionSpace, and Coefficient, unchanged (note that MixedMesh subclasses AbstractDomain); e.g., we can do c = Coefficient(FunctionSpace(MixedMesh, MixedElement)), and directly use c in an expression. This allows us to not worry about mixedness in most parts of UFL and integrate mixed problem almost seamlessly. Indeed, in this PR, core changes only happened in GradRuleset.reference_value(), GradRuleset.reference_grad(), and pullback.apply(), which are low-level methods, and virtually no change was needed in high-level functions, and, to support mixed domain problem, Firedrake does not need to introduce new interface. The following is the basic usage in Firedrake:

mesh = ...
subm = Submesh(mesh, ...)
V0 = FunctionSpace(mesh, "CG", 1)
V1 = FunctionSpace(subm, "CG", 1)
V = V0 * V1
u = Function(V)
v = TestFunction(V)
v0, v1 = split(v)
dx1 = Measure("cell", domain=subm)
a = inner(u[0], v1) * dx1
A = assemble(a)

MixedMesh` might not be very intuitive, but I think it is the right abstraction.

@jorgensd
Copy link
Member

jorgensd commented Jan 7, 2025

For instance, we can not define Coefficient on a MixedFunctionSpace; we need to use the (special-cased) Coefficients function, which returns a list of Coefficients. We thus can not use high-level functions, such as inner and split, for coefficients on MixedFunctionSpaces. Components of an Argument should also be consistently represented by Indexed(argument, index) in UFL instead of by part, I think.

If the only thing that makes MixedMesh favorable is the possibility of creating a coefficient over all spaces, i believe we should rather make this possible with MixedFunctionSpace than adding a whole new class.

On the other hand, if everyone agrees that MixedMesh is the way to go, we should remove MixedFunctionSpace, and adapt the code in DOLFINx/FFCx to handle it. I see no benefit of having both code paths (as long as extract_blocks would
work on a MixedMesh form.

and multiple meshes + multiple spaces.
Current MixedFunctionSpace approach handles the last case differently inside UFL (and inside DOLFINx, I believe).

MixedFunctionSpace keeps a list of function spaces, in a similar fashion to what happens under the hood in the FunctionSpace(MixedMesh), thus I don't think the handling is too different in the two approaches.

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?

My plan is to introduce MixedCell that should be shared by the MixedMesh and the MixedElement for mixed problems with multiple cell types.

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).

@dham
Copy link
Collaborator

dham commented Jan 7, 2025

My plan is to introduce MixedCell that should be shared by the MixedMesh and the MixedElement for mixed problems with multiple cell types.

How would this compose with a mesh which didn't have a unique cell type (because it contains triangles and quads, for example)?

@dham
Copy link
Collaborator

dham commented Jan 7, 2025

I think we should separate the name from the object. E.g. we could call it a MeshSequence or a MultiMesh.

I think the object disagreement here is caused by a long-standing difference of viewpoint about ufl.MixedFunctionSpace, which Firedrake has never used. We've always just created a FunctionSpace on a mixed element. This is partly because we distinguish between topological and geometric FunctionSpaces in order to break the inheritance cycle caused by the fact that Firedrake has first class coordinates (i.e. the mesh coordinates are a Function).

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.

@dham
Copy link
Collaborator

dham commented Jan 7, 2025

My plan is to introduce MixedCell that should be shared by the MixedMesh and the MixedElement for mixed problems with multiple cell types.

How would this compose with a mesh which didn't have a unique cell type (because it contains triangles and quads, for example)?

Actually, if we follow the pattern, then the elements on a mixed topology domain are themselves a sort of mixed element. No problem.

@ksagiyam
Copy link
Contributor Author

ksagiyam commented Jan 7, 2025

@jorgensd I do not think merely allowing for Coefficient(MixedFunctionSpace) will solve all issues; as long as MixedFunctionSpace does not subclass FunctionSpace and does not have ufl_domain, I think we will need to special case functions/methods in UFL for MixedFunctionSpace just as Coefficients function currently does. As I mentioned, introduction of MixedMesh allows UFL to be unaware of the mixed spaces in the most part, and it only needs to special case methods at the very bottom, i.e. GradRuleset.reference_value(), GradRuleset.reference_grad(), and pullback.apply().

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 flatten_domain_element are written with this in mind as you might have noticed. I do not think consideration of such nested mixed spaces would affect the mixed-problem design in UFL, though.

@jhale I think the use of Mixed in MixedMesh is consistent with the use of Mixed in MixedElement. If we consider assembling (i, j)-block of a matrix, we can view it as mixing mixed_mesh[i] and mixed_mesh[j] as well as mixing mixed_element[i] and mixed_element[j].

But I agree that it is confusing. As @dham suggested, MeshSequence or MultiMesh is also fine by me.

@jorgensd
Copy link
Member

jorgensd commented Jan 7, 2025

@jorgensd I do not think merely allowing for Coefficient(MixedFunctionSpace) will solve all issues; as long as MixedFunctionSpace does not subclass FunctionSpace and does not have ufl_domain, I think we will need to special case functions/methods in UFL for MixedFunctionSpace just as Coefficients function currently does. As I mentioned, introduction of MixedMesh allows UFL to be unaware of the mixed spaces in the most part, and it only needs to special case methods at the very bottom, i.e. GradRuleset.reference_value(), GradRuleset.reference_grad(), and pullback.apply().

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 flatten_domain_element are written with this in mind as you might have noticed. I do not think consideration of such nested mixed spaces would affect the mixed-problem design in UFL, though.

@jhale I think the use of Mixed in MixedMesh is consistent with the use of Mixed in MixedElement. If we consider assembling (i, j)-block of a matrix, we can view it as mixing mixed_mesh[i] and mixed_mesh[j] as well as mixing mixed_element[i] and mixed_element[j].

But I agree that it is confusing. As @dham suggested, MeshSequence or MultiMesh is also fine by me.

Let's not use MultiMesh, as that has been used in FEniCS before to mean hierarchies of meshes.

@jhale
Copy link
Member

jhale commented Jan 8, 2025

MeshSequence is clearer and at least to my understanding it is consistent with Python's abstract base class terminology collections.abc.Sequence.

@ksagiyam I see your analogy between MixedMesh and MixedElement at the form level, however, MixedElement was named after a widely established and accepted terminology from the finite element literature. We don't have this for MixedMesh, and so my preference is to take precise terminology from elsewhere.

@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch 5 times, most recently from 2073e67 to bf1fcff Compare January 10, 2025 15:53
@ksagiyam
Copy link
Contributor Author

Thanks, all. Yes, let us use MeshSequence. The followings are the changes I have made according to the suggestions:

  • Addressed @jorgensd 's suggestions,
  • Renamed MixedMesh MeshSequence,
  • Removed ufl_id from MeshSequence; I realised that my Firedrake branch broke after a recent rebase, and found that it was caused by distinguishing two MeshSequence instances made from the same set of Meshes. Thinking about this, I thought ufl_id in MeshSequence is redundant; please see updated MeshSequence.__repr__() and MeshSequence._ufl_hash_data_(). I also added a test to highlight this point.

@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.

@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch 2 times, most recently from b1f293d to 3fe26ca Compare January 15, 2025 22:37
Co-authored-by: Jørgen Schartum Dokken <[email protected]>
@ksagiyam ksagiyam force-pushed the ksagiyam/introduce_mixed_map_0 branch from 3fe26ca to df25c20 Compare January 20, 2025 11:06
@ksagiyam ksagiyam changed the title introduce MixedMesh class introduce MeshSequence class Jan 20, 2025
@ksagiyam
Copy link
Contributor Author

@jhale @jorgensd Can we have this possibly merged?

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.

6 participants