Skip to content

Add support for non-cartesian manifolds in codim problems - #1264

Open
ee-nn wants to merge 7 commits into
festim-dev:mainfrom
ee-nn:1263-non-cartesian-manifolds
Open

ee-nn wants to merge 7 commits into
festim-dev:mainfrom
ee-nn:1263-non-cartesian-manifolds

Conversation

@ee-nn

@ee-nn ee-nn commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Description

v2.2-rc.2 released co-dimensional subdomains but only for cartesian coordinates. This adds functionality for cylindrical and cartesian coordinates.

Related Issues

Fixes #1263

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 🔨 Code refactoring (no functional changes, no API changes)
  • 📝 Documentation update
  • ✅ Test update (adding missing tests or correcting existing tests)
  • 🔧 Build/CI configuration change

Testing

  • All existing tests pass locally (pytest)
  • I have added new tests that prove my fix is effective or that my feature works

Code Quality Checklist

  • My code follows the code style of this project (Ruff formatted: ruff format .)
  • My code passes linting checks (ruff check .)
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas

Documentation

  • I have updated the documentation accordingly (if applicable)
  • I have added docstrings to new functions/classes following the project conventions

Screenshots/Examples

Here is a small mwe:

from mpi4py import MPI

import dolfinx
import numpy as np

import festim as F


def run(name, mesh):
    bulk = F.VolumeSubdomain(id=1, material=F.Material(D_0=1, E_D=0))
    surface = F.VolumeSubdomain(  # the co-dim part 
        id=2,
        dim=mesh.vdim - 1,
        locator=lambda x: np.isclose(x[0], 1.0),
        material=F.Material(D_0=0.1, E_D=0),
    )
    c_bulk = F.Species("bulk", subdomains=[bulk])
    c_surface = F.Species("surface", subdomains=[surface])
    dependencies = {"cb": c_bulk, "cs": c_surface}

    # Hydrogen initially in the bulk transfers to the initially empty surface.
    # The pipe starts with an axial gradient so its surface profile is nonuniform.
    model = F.HydrogenTransportProblemDiscontinuous(
        mesh=mesh,
        subdomains=[bulk, surface],
        species=[c_bulk, c_surface],
        temperature=500,
        initial_conditions=[
            F.InitialConcentration(
                value=(lambda x: 1 + x[1]) if name == "pipe" else 1.0,
                species=c_bulk,
                volume=bulk,
            ),
        ],
        boundary_conditions=[
            F.ParticleFluxBC(
                subdomain=surface,
                species=c_bulk,
                value=lambda cb, cs: cs - cb,  # assume k = 1
                species_dependent_value=dependencies,
            ),
        ],
        sources=[
            F.ParticleSource(
                volume=surface,
                species=c_surface,
                value=lambda cb, cs: cb - cs,
                species_dependent_value=dependencies,
            ),
        ],
        # A codim manifold carries its own equation, so use a volume quantity.
        exports=[
            F.AverageVolume(
                field=c_surface,
                volume=surface,
                filename=f"{name}_surface.csv",
            ),
        ],
        settings=F.Settings(
            transient=True,
            stepsize=0.01,
            final_time=0.2,
            atol=1e-10,
            rtol=1e-10,
        ),
    )
    model.initialise()
    model.run()


# Pipe: inner radius 0.5, outer radius 1, length 2; outer wall has dim=1.
pipe = dolfinx.mesh.create_rectangle(
    MPI.COMM_WORLD, [np.array([0.5, 0.0]), np.array([1.0, 2.0])], [8, 24]
)
run("pipe", F.Mesh(pipe, coordinate_system="cylindrical"))

# Ball: radius 1; outer spherical surface has dim=0 in the radial mesh.
ball = dolfinx.mesh.create_interval(MPI.COMM_WORLD, 32, [0.0, 1.0])
run("ball", F.Mesh(ball, coordinate_system="spherical"))

@ee-nn ee-nn self-assigned this Sep 12, 2026
@codecov

codecov Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.03%. Comparing base (3e37dab) to head (822f47f).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1264      +/-   ##
==========================================
+ Coverage   95.86%   96.03%   +0.16%     
==========================================
  Files          57       57              
  Lines        5036     5065      +29     
==========================================
+ Hits         4828     4864      +36     
+ Misses        208      201       -7     
Flag Coverage Δ
dolfinx-v0.10.0 93.08% <100.00%> (+0.17%) ⬆️
dolfinx-v0.11.0 95.89% <98.41%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/festim/mesh/mesh.py
Comment on lines +20 to +31
def integration_weight(self, mesh):
"""Physical measure per unit mesh measure, assuming full angular coverage.

Use the integration domain's coordinates, including when it is a manifold
submesh. Cylindrical 1D quantities are per unit axial length.
"""
if self == CoordinateSystem.CARTESIAN:
return 1
r = spatial_coordinate(mesh)[0]
if self == CoordinateSystem.CYLINDRICAL:
return 2 * math.pi * r
return 4 * math.pi * r**2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good idea to store this in the CoordinateSystem class.

def define_meshtags(self, surface_subdomains, volume_subdomains, interfaces=None):
# check if all borders are defined
self.check_borders(volume_subdomains)
self.check_borders([v for v in volume_subdomains if v.codim(self.vdim) == 0])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch. Is there a test catching this bug?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I don't believe so, I can add one

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a small direct unit test for this just to be safe, although several of the system tests added do fail if this bug is present

Comment thread src/festim/advection.py
"Lagrange",
function_space.mesh.topology.cell_name(),
1,
0 if function_space.mesh.topology.dim == 0 else 1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • advection in 0D?
  • I don't even think basix allows ("Lagrange", 0), is this tested?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sorry I glossed over this initially! You're right I don't think this makes sense, perhaps we can replace this with a warning/error, similar to your comment further down.

I did double-check the syntax and basix.ufl.element("Lagrange", "point", 0) does work. "point" should already be what is set from dolfinx.mesh.create_submesh.

Comment thread src/festim/helpers.py
Comment on lines +16 to +37
def spatial_coordinate(mesh):
"""Coordinates on an integration mesh, including a zero-dimensional submesh.

FFCx 0.11 cannot compile SpatialCoordinate on a vertex cell (its coordinate
table is identically one). A P0 coefficient stores the exact coordinates at
each point, also when a point subdomain contains several disconnected points.
"""
domain = mesh if isinstance(mesh, ufl.Mesh) else mesh.ufl_domain()
# UFL exposed this as a method before the release bundled with DOLFINx 0.11.
tdim = domain.topological_dimension
if callable(tdim):
tdim = tdim()
if tdim > 0:
return ufl.SpatialCoordinate(domain)
if isinstance(mesh, ufl.Mesh):
mesh = dolfinx.mesh.Mesh(domain.ufl_cargo(), domain)
gdim = mesh.geometry.dim
V = fem.functionspace(mesh, ("P", 0, (gdim,)))
coordinate = fem.Function(V)
coordinate.interpolate(lambda x: x[:gdim])
coordinate.x.scatter_forward()
return coordinate

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So this is only to catch the situation where you have a vertex cell (a point mesh)?

In that case, could we not have a try/except instead?

try:
    return ufl.SpatialCoordinate(domain)
except [INSERT ERROR WITH 0D MESH]:
   tdim = ....
   V = ....
    coordinate = ...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes we could do something like that instead, however after a bit of testing I realize that we'd need to wrap ufl.SpatialCoordinate with dolfinx.fem.form() because it only fails at form compilation

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I left it as-is for now since trying to return ufl.SpatialCoordinate(domain) won't directly trip a try/except, but let me know if you think it would still be good to have a try/except block here

Comment on lines +1773 to +1775
# A radial surface is a point in a 1D mesh. Basix only supports P0 there.
if subdomain.submesh.topology.dim == 0:
element_degree = 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we agree that this is not necessarily related to non-cartesian manifolds but 0D manifolds in general yes?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes that's my understanding as well

Comment on lines +2450 to +2452
if subdomain.submesh.topology.dim == 0:
# A point has no tangential direction in which a species can drift.
continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we raise a warning or maybe even an error here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I agree, I think a warning might be good

@ee-nn

ee-nn commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

@RemDelaporteMathurin Let me know if this looks okay now, thanks for all the feedback so far! Once it's merged it should make #1271 easier

@RemDelaporteMathurin

Copy link
Copy Markdown
Collaborator

@ee-nn i'm traveling today so I won't have time to review it again. @hhy2022 Could you base this other PR off this branch instead?

@hhy2022

hhy2022 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Sure! I will base my PR on this branch when I start working on #1271. @ee-nn @RemDelaporteMathurin

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.

Allow non-cartesian meshes with manifolds

3 participants