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

Average surface and volume derived quantities in Cylindrical and Spherical #820

Merged
merged 31 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
fb31540
first effort
jhdark Jul 25, 2024
5fb6032
add new classes
jhdark Jul 25, 2024
cd7c930
remove comments and testing
jhdark Jul 26, 2024
e82da3a
fix
jhdark Jul 26, 2024
740e8e6
surface not volume
jhdark Jul 26, 2024
b0ce4fb
fix documentation
jhdark Jul 26, 2024
a12c519
fix azimuth range spherical and doc strings
jhdark Jul 26, 2024
307b95b
no more than pi in range
jhdark Jul 26, 2024
a83849e
ranges cancel out surface
jhdark Jul 26, 2024
4bc4a6a
remove angles
jhdark Jul 26, 2024
861fb61
Merge branch 'main' into cyl_and_sph_avg_quantities
jhdark Aug 5, 2024
bb1a023
update doc strings
jhdark Aug 5, 2024
d4fe1c2
remove print and quit
jhdark Aug 5, 2024
d904cb3
added tests
jhdark Aug 5, 2024
b96300d
rename tests
jhdark Aug 5, 2024
41b90f1
change export name
jhdark Aug 5, 2024
5944b68
test average volume
jhdark Aug 5, 2024
e747d52
dont need haevy mesh
jhdark Aug 5, 2024
1c22343
Merge branch 'main' into cyl_and_sph_avg_quantities
jhdark Jan 10, 2025
636a322
show units now default
jhdark Jan 10, 2025
87c4312
simplify avg sph surf class
jhdark Jan 10, 2025
d4b6979
fix tests
jhdark Jan 10, 2025
152a9c5
remove unused imports
jhdark Jan 10, 2025
1e5f411
Update festim/exports/derived_quantities/average_surface.py
jhdark Jan 10, 2025
296d744
test avg surface on a rhombus
jhdark Jan 10, 2025
58bd814
add allowed meshes property
jhdark Jan 14, 2025
e5f0a6a
accept spherical meshes for evg surface
jhdark Jan 14, 2025
ce2f148
remove uneccesary test
jhdark Jan 14, 2025
47f853e
test allowed meshes
jhdark Jan 14, 2025
301cba0
additional tests for avg volume
jhdark Jan 14, 2025
10fc3b0
simplify tests
jhdark Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions festim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,22 @@
)
from .exports.derived_quantities.hydrogen_flux import HydrogenFlux
from .exports.derived_quantities.thermal_flux import ThermalFlux
from .exports.derived_quantities.average_volume import AverageVolume
from .exports.derived_quantities.average_volume import (
AverageVolume,
AverageVolumeCylindrical,
AverageVolumeSpherical,
)
from .exports.derived_quantities.maximum_volume import MaximumVolume
from .exports.derived_quantities.minimum_volume import MinimumVolume
from .exports.derived_quantities.minimum_surface import MinimumSurface
from .exports.derived_quantities.maximum_surface import MaximumSurface
from .exports.derived_quantities.total_surface import TotalSurface
from .exports.derived_quantities.total_volume import TotalVolume
from .exports.derived_quantities.average_surface import AverageSurface
from .exports.derived_quantities.average_surface import (
AverageSurface,
AverageSurfaceCylindrical,
AverageSurfaceSpherical,
)
from .exports.derived_quantities.point_value import PointValue
from .exports.derived_quantities.adsorbed_hydrogen import AdsorbedHydrogen

Expand Down
58 changes: 57 additions & 1 deletion festim/exports/derived_quantities/average_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(self, field, surface) -> None:

@property
def allowed_meshes(self):
return ["cartesian"]
return ["cartesian", "spherical"]

@property
def export_unit(self):
Expand All @@ -51,3 +51,59 @@ def compute(self):
return f.assemble(self.function * self.ds(self.surface)) / f.assemble(
1 * self.ds(self.surface)
)


class AverageSurfaceCylindrical(AverageSurface):
RemDelaporteMathurin marked this conversation as resolved.
Show resolved Hide resolved
"""
Computes the average value of a field on a given surface
int(f ds) / int (1 * ds)
ds is the surface measure in cylindrical coordinates.
ds = r dr dtheta

Args:
field (str, int): the field ("solute", 0, 1, "T", "retention")
surface (int): the surface id

Attributes:
field (str, int): the field ("solute", 0, 1, "T", "retention")
surface (int): the surface id
title (str): the title of the derived quantity
show_units (bool): show the units in the title in the derived quantities
file
function (dolfin.function.function.Function): the solution function of
RemDelaporteMathurin marked this conversation as resolved.
Show resolved Hide resolved
the field
r (ufl.indexed.Indexed): the radius of the cylinder

Notes:
Units are in H/m3 for hydrogen concentration and K for temperature
"""

def __init__(self, field, surface) -> None:
super().__init__(field=field, surface=surface)
self.r = None

@property
def allowed_meshes(self):
return ["cylindrical"]

def compute(self):

if self.r is None:
mesh = (
self.function.function_space().mesh()
) # get the mesh from the function
rthetaz = f.SpatialCoordinate(mesh) # get the coordinates from the mesh
self.r = rthetaz[0] # only care about r here

# dS_z = r dr dtheta , assuming axisymmetry dS_z = theta r dr
# dS_r = r dz dtheta , assuming axisymmetry dS_r = theta r dz
# in both cases the expression with self.dx is the same

avg_surf = f.assemble(
self.function * self.r * self.ds(self.surface)
) / f.assemble(1 * self.r * self.ds(self.surface))

return avg_surf


AverageSurfaceSpherical = AverageSurface
92 changes: 92 additions & 0 deletions festim/exports/derived_quantities/average_volume.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AverageVolume(VolumeQuantity):
file
function (dolfin.function.function.Function): the solution function of
the field
r (ufl.indexed.Indexed): the radius of the cylinder

.. note::
Units are in H/m3 for hydrogen concentration and K for temperature
Expand Down Expand Up @@ -50,3 +51,94 @@ def compute(self):
return f.assemble(self.function * self.dx(self.volume)) / f.assemble(
1 * self.dx(self.volume)
)


class AverageVolumeCylindrical(AverageVolume):
"""
Computes the average value of a field in a given volume
int(f dx) / int (1 * dx)
dx is the volume measure in cylindrical coordinates.
dx = r dr dz dtheta

Note: for particle fluxes J is given in H/s, for heat fluxes J is given in W

Args:
field (str, int): the field ("solute", 0, 1, "T", "retention")
volume (int): the volume id

Attributes:
field (str, int): the field ("solute", 0, 1, "T", "retention")
volume (int): the volume id
title (str): the title of the derived quantity
show_units (bool): show the units in the title in the derived quantities
file
function (dolfin.function.function.Function): the solution function of
the field
r (ufl.indexed.Indexed): the radius of the sphere
"""

def __init__(self, field, volume) -> None:
super().__init__(field=field, volume=volume)
self.r = None

@property
def allowed_meshes(self):
return ["cylindrical"]

def compute(self):

if self.r is None:
mesh = (
self.function.function_space().mesh()
) # get the mesh from the function
rthetaz = f.SpatialCoordinate(mesh) # get the coordinates from the mesh
self.r = rthetaz[0] # only care about r here

avg_vol = f.assemble(
self.function * self.r * self.dx(self.volume)
) / f.assemble(1 * self.r * self.dx(self.volume))

return avg_vol


class AverageVolumeSpherical(AverageVolume):
"""
Computes the average value of a field in a given volume
int(f dx) / int (1 * dx)
dx is the volume measure in cylindrical coordinates.
dx = rho dtheta dphi

Note: for particle fluxes J is given in H/s, for heat fluxes J is given in W

Args:
field (str, int): the field ("solute", 0, 1, "T", "retention")
volume (int): the volume id
title (str): the title of the derived quantity
show_units (bool): show the units in the title in the derived quantities
file
function (dolfin.function.function.Function): the solution function of
the field
"""

def __init__(self, field, volume) -> None:
super().__init__(field=field, volume=volume)
self.r = None

@property
def allowed_meshes(self):
return ["spherical"]

def compute(self):

if self.r is None:
mesh = (
self.function.function_space().mesh()
) # get the mesh from the function
rthetaphi = f.SpatialCoordinate(mesh) # get the coordinates from the mesh
self.r = rthetaphi[0] # only care about r here

avg_vol = f.assemble(
self.function * self.r**2 * self.dx(self.volume)
) / f.assemble(1 * self.r**2 * self.dx(self.volume))

return avg_vol
1 change: 0 additions & 1 deletion test/simulation/test_initialise.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ def test_TXTExport_times_added_to_milestones(tmpdir):
F.SurfaceFlux(field="solute", surface=1),
F.TotalVolume(field="solute", volume=1),
F.TotalSurface(field="solute", surface=1),
F.AverageSurface(field="solute", surface=1),
F.AverageVolume(field="solute", volume=1),
F.HydrogenFlux(surface=1),
F.ThermalFlux(surface=1),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
from festim import AverageSurface
from festim import (
AverageSurface,
AverageSurfaceCylindrical,
AverageSurfaceSpherical,
x,
y,
)
import fenics as f
import pytest
import numpy as np
from sympy.printing import ccode


@pytest.mark.parametrize("field, surface", [("solute", 1), ("T", 2)])
Expand Down Expand Up @@ -46,3 +54,147 @@ def test_h_average(self):
)
computed = self.my_average.compute()
assert computed == expected


@pytest.mark.parametrize("radius", [1, 4])
@pytest.mark.parametrize("r0", [3, 5])
@pytest.mark.parametrize("height", [2, 7])
def test_compute_cylindrical(r0, radius, height):
"""
Test that AverageSurfaceCylindrical computes the value correctly on a hollow cylinder

Args:
r0 (float): internal radius
radius (float): cylinder radius
height (float): cylinder height
"""
# creating a mesh with FEniCS
r1 = r0 + radius
z0, z1 = 0, height

mesh_fenics = f.RectangleMesh(f.Point(r0, z0), f.Point(r1, z1), 10, 10)

top_surface = f.CompiledSubDomain(
f"on_boundary && near(x[1], {z1}, tol)", tol=1e-14
)
surface_markers = f.MeshFunction(
"size_t", mesh_fenics, mesh_fenics.topology().dim() - 1
)
surface_markers.set_all(0)
ds = f.Measure("ds", domain=mesh_fenics, subdomain_data=surface_markers)
# Surface ids
top_id = 2
top_surface.mark(surface_markers, top_id)

my_export = AverageSurfaceCylindrical("solute", top_id)
V = f.FunctionSpace(mesh_fenics, "P", 1)
RemDelaporteMathurin marked this conversation as resolved.
Show resolved Hide resolved
c_fun = lambda r: 2 * r

expr = f.Expression(
ccode(c_fun(x)),
degree=1,
)
my_export.function = f.interpolate(expr, V)
my_export.ds = ds

expected_value = 4 / 3 * (r1**3 - r0**3) / (r1**2 - r0**2)

computed_value = my_export.compute()

assert np.isclose(computed_value, expected_value)


@pytest.mark.parametrize("radius", [2, 4])
@pytest.mark.parametrize("r0", [3, 5])
def test_compute_spherical(r0, radius):
"""
Test that AverageSurfaceSpherical computes the average value correctly
on a hollow sphere

Args:
r0 (float): internal radius
radius (float): sphere radius
"""
# creating a mesh with FEniCS
r1 = r0 + radius
mesh_fenics = f.IntervalMesh(10, r0, r1)

# marking physical groups (volumes and surfaces)
right_surface = f.CompiledSubDomain(
f"on_boundary && near(x[0], {r1}, tol)", tol=1e-14
)
surface_markers = f.MeshFunction(
"size_t", mesh_fenics, mesh_fenics.topology().dim() - 1
)
surface_markers.set_all(0)
# Surface ids
right_id = 2
right_surface.mark(surface_markers, right_id)
ds = f.Measure("ds", domain=mesh_fenics, subdomain_data=surface_markers)

my_export = AverageSurfaceSpherical("solute", right_id)
V = f.FunctionSpace(mesh_fenics, "P", 1)
RemDelaporteMathurin marked this conversation as resolved.
Show resolved Hide resolved
c_fun = lambda r: 4 * r
expr = f.Expression(
ccode(c_fun(x)),
degree=1,
)
my_export.function = f.interpolate(expr, V)

my_export.ds = ds

expected_value = 4 * r1

computed_value = my_export.compute()

assert np.isclose(computed_value, expected_value)


def test_average_surface_cylindrical_title_no_units_solute():
"""A simple test to check that the title is set correctly in
festim.AverageSurfaceCylindrical with a solute field without units"""

my_export = AverageSurfaceCylindrical("solute", 4)
assert my_export.title == "Average solute surface 4 (H m-3)"


def test_average_surface_cylindrical_title_no_units_temperature():
"""A simple test to check that the title is set correctly in
festim.AverageSurfaceCylindrical with a T field without units"""

my_export = AverageSurfaceCylindrical("T", 5)
assert my_export.title == "Average T surface 5 (K)"


def test_average_surface_spherical_title_no_units_solute():
"""A simple test to check that the title is set correctly in
festim.AverageSurfaceSpherical with a solute field without units"""

my_export = AverageSurfaceSpherical("solute", 6)
assert my_export.title == "Average solute surface 6 (H m-3)"


def test_average_surface_spherical_title_no_units_temperature():
"""A simple test to check that the title is set correctly in
festim.AverageSurfaceSpherical with a T field without units"""

my_export = AverageSurfaceSpherical("T", 9)
assert my_export.title == "Average T surface 9 (K)"


def test_avg_surf_cylindrical_allow_meshes():
"""A simple test to check cylindrical meshes are the only
meshes allowed when using AverageSurfaceCylindrical"""

my_export = AverageSurfaceCylindrical("solute", 2)

assert my_export.allowed_meshes == ["cylindrical"]


def test_avg_surf_spherical_allow_meshes():
"""A simple test to check spherical meshes are one of the
meshes allowed when using AverageSurfaceSpherical"""

my_export = AverageSurfaceSpherical("T", 6)

assert "spherical" in my_export.allowed_meshes
Loading
Loading