Skip to content

bug fix for min/max values not working on discontinuous problem - #1226

Open
ee-nn wants to merge 11 commits into
festim-dev:mainfrom
ee-nn:1225-min-max-export-fix
Open

bug fix for min/max values not working on discontinuous problem#1226
ee-nn wants to merge 11 commits into
festim-dev:mainfrom
ee-nn:1225-min-max-export-fix

Conversation

@ee-nn

@ee-nn ee-nn commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Description

Provides a fix for issue #1225.

Summary

Essentially #1225 was the result of two separate problems:

  1. initialise_exports() of HydrogenTransportProblemDiscontinuous overrides the base method from HydrogenTransportProblem, but it never initializes its own export.volume_meshtags or export.facet_meshtags. Since it doesn't invoke super() either, these attributes of export aren't carried over to the discontinuous class.

  2. Even with the meshtags attached, compute() in Min/MaxVolume and Min/MaxSurface read self.field.post_processing_solution, which stays None in the discontinuous case because the solution lives per-submesh in species.subdomain_to_post_processing_solution[subdomain].

Motivation and Context

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

Specifically, new system test for the discontinuous case (which is essentially the MWE listed in #1225 - not sure if this is overkill?), and some smaller assertion tests to ensure that meshtags are always defined.

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

@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.64%. Comparing base (d40ae63) to head (d107866).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1226      +/-   ##
==========================================
+ Coverage   95.38%   95.64%   +0.25%     
==========================================
  Files          53       53              
  Lines        4140     4202      +62     
==========================================
+ Hits         3949     4019      +70     
+ Misses        191      183       -8     

☔ 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 on lines +2138 to +2164
# the extrema exports read the solution on the submesh of the volume
# subdomain, so check that the species is actually defined there.
# a field that is not a Species with a list of subdomains (a bare name,
# or a species generated from a trap) is skipped: it fails earlier, for
# unrelated reasons
is_extremum = isinstance(
export,
exports.MaximumVolume
| exports.MinimumVolume
| exports.MaximumSurface
| exports.MinimumSurface,
)
if is_extremum and isinstance(export.field, _species.Species):
if isinstance(export, exports.SurfaceQuantity):
volume = self.surface_to_volume[export.surface]
location = f"surface {export.surface.id}"
else:
volume = export.volume
location = f"volume {volume.id}"
subdomains = export.field.subdomains
if isinstance(subdomains, list) and volume not in subdomains:
raise ValueError(
f"Cannot compute {export.title}: species "
f"{export.field.name} is not defined in the volume subdomain "
f"{volume.id} that {location} belongs to"
)

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.

This just provides some error catching with nice-looking raises so could be deleted if desired

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.

I think it's good to catch this early

Comment on lines +471 to +482
bot_min = F.MinimumVolume(
field=H, volume=bottom_volume, filename=f"{tmpdir}/bot_min.csv"
)
bot_max = F.MaximumVolume(field=H, volume=bottom_volume)
top_min = F.MinimumVolume(field=H, volume=top_volume)
top_max = F.MaximumVolume(field=H, volume=top_volume)
bot_surf_min = F.MinimumSurface(
field=H, surface=bottom_surface, filename=f"{tmpdir}/bot_surf_min.csv"
)
bot_surf_max = F.MaximumSurface(field=H, surface=bottom_surface)
top_surf_min = F.MinimumSurface(field=H, surface=top_surface)
top_surf_max = F.MaximumSurface(field=H, surface=top_surface)

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.

Might be a little redundant, perhaps only the bottom domain could be checked?

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.

I think it's ok to look out for everything

@RemDelaporteMathurin RemDelaporteMathurin left a comment

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.

Thanks for this @ee-nn !!! A first pass of comments before I go more in depth

Comment thread src/festim/exports/maximum_surface.py Outdated
meshtags = self.facet_meshtags if facet_meshtags is None else facet_meshtags

if meshtags is None:
raise ValueError(

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.

Could be an assertion instead with a message if false

values = solution.x.array[dofs]

# a process may hold no dof of the surface at all, np.max would then raise
local_max = np.max(values) if values.size > 0 else -np.inf

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

Comment on lines +2138 to +2164
# the extrema exports read the solution on the submesh of the volume
# subdomain, so check that the species is actually defined there.
# a field that is not a Species with a list of subdomains (a bare name,
# or a species generated from a trap) is skipped: it fails earlier, for
# unrelated reasons
is_extremum = isinstance(
export,
exports.MaximumVolume
| exports.MinimumVolume
| exports.MaximumSurface
| exports.MinimumSurface,
)
if is_extremum and isinstance(export.field, _species.Species):
if isinstance(export, exports.SurfaceQuantity):
volume = self.surface_to_volume[export.surface]
location = f"surface {export.surface.id}"
else:
volume = export.volume
location = f"volume {volume.id}"
subdomains = export.field.subdomains
if isinstance(subdomains, list) and volume not in subdomains:
raise ValueError(
f"Cannot compute {export.title}: species "
f"{export.field.name} is not defined in the volume subdomain "
f"{volume.id} that {location} belongs to"
)

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.

I think it's good to catch this early

Comment on lines +471 to +482
bot_min = F.MinimumVolume(
field=H, volume=bottom_volume, filename=f"{tmpdir}/bot_min.csv"
)
bot_max = F.MaximumVolume(field=H, volume=bottom_volume)
top_min = F.MinimumVolume(field=H, volume=top_volume)
top_max = F.MaximumVolume(field=H, volume=top_volume)
bot_surf_min = F.MinimumSurface(
field=H, surface=bottom_surface, filename=f"{tmpdir}/bot_surf_min.csv"
)
bot_surf_max = F.MaximumSurface(field=H, surface=bottom_surface)
top_surf_min = F.MinimumSurface(field=H, surface=top_surface)
top_surf_max = F.MaximumSurface(field=H, surface=top_surface)

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.

I think it's ok to look out for everything

Comment thread test/test_minimum_surface.py Outdated
)

with pytest.raises(ValueError, match="facet meshtags are required"):
my_export.compute()

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.

If meshtags are always required why are they not a mandatory argument of compute?

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.

Good point, I can think about this more and update

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.

Pushed an implementation, let me know what you think

ee-nn added a commit to ee-nn/FESTIM that referenced this pull request Aug 10, 2026
ee-nn added a commit to ee-nn/FESTIM that referenced this pull request Aug 11, 2026
Comment thread src/festim/exports/minimum_volume.py Outdated
Comment on lines +33 to +34
u: dolfinx.fem.Function | None = None,
volume_meshtags: dolfinx.mesh.MeshTags | None = None,

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.

For a given problem, one would never invoke compute() with different volume mesthags and different us right? I think they should be attributes instead and set inside problem.initialise() somewhere. (I think we have a initialise_exports method)

Comment thread src/festim/exports/minimum_volume.py Outdated
defined on. This is the expected behaviour for the discontinuous
problem, where each volume subdomain owns its own submesh.
"""
solution = self.field.post_processing_solution if u is None else u

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.

this could be a property

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.

or a method rather

Comment thread src/festim/exports/minimum_volume.py Outdated
problem, where each volume subdomain owns its own submesh.
"""
solution = self.field.post_processing_solution if u is None else u
meshtags = self.volume_meshtags if volume_meshtags is None else volume_meshtags

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.

this should be an attribute

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.

Does this only apply to the volume_meshtag class and then we keep the facet_meshtags argument for the surface?

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.

i think this would apply to both Volume quantities and Surface Quantities (if that's what you're asking)

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.

Ah yeah that's what I was getting at, thanks 🙂

Comment thread src/festim/exports/minimum_volume.py Outdated
defined on. This is the expected behaviour for the discontinuous
problem, where each volume subdomain owns its own submesh.
"""
solution = self.field.post_processing_solution if u is None else u

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.

at the beginning of compute we should have an assertion to make sure that meshtags have been given

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.

This would only be needed for when we are not on a submesh right? If we have a property like self.is_submesh as you suggest for the if meshtags is None statement, then this would go in the else branch of the conditional block, no?

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.

I guess so yes!

Comment thread src/festim/exports/minimum_volume.py Outdated
)

self.value = mesh.comm.allreduce(np.min(solution.x.array[dofs]), op=MPI.MIN)
if meshtags is None:

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.

this should maybe be a property too like self.is_submesh.

Again, one day we'll get rid of the distinction between discontinous and non-discontinuous #1191

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.

2 participants