Skip to content

Commit db9ae13

Browse files
Merge pull request #1013 from festim-dev/profile_1d
Profile 1d export
2 parents 34ad136 + 42ce9cc commit db9ae13

File tree

5 files changed

+205
-9
lines changed

5 files changed

+205
-9
lines changed

src/festim/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from .exports.maximum_volume import MaximumVolume
3030
from .exports.minimum_surface import MinimumSurface
3131
from .exports.minimum_volume import MinimumVolume
32+
from .exports.profile_1d import Profile1DExport
3233
from .exports.surface_flux import SurfaceFlux
3334
from .exports.surface_quantity import SurfaceQuantity
3435
from .exports.total_surface import TotalSurface

src/festim/exports/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .maximum_volume import MaximumVolume
55
from .minimum_surface import MinimumSurface
66
from .minimum_volume import MinimumVolume
7+
from .profile_1d import Profile1DExport
78
from .surface_flux import SurfaceFlux
89
from .surface_quantity import SurfaceQuantity
910
from .total_surface import TotalSurface
@@ -20,6 +21,7 @@
2021
"MaximumVolume",
2122
"MinimumSurface",
2223
"MinimumVolume",
24+
"Profile1DExport",
2325
"SurfaceFlux",
2426
"SurfaceQuantity",
2527
"TotalSurface",

src/festim/exports/profile_1d.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import numpy as np
2+
from festim.species import Species
3+
from festim.subdomain import VolumeSubdomain
4+
5+
6+
class Profile1DExport:
7+
"""Class to export 1D profiles of a field in a simulation.
8+
9+
Args:
10+
field: the species for which the profile is computed.
11+
subdomain: the volume subdomain to compute the profile on. If None, the profile
12+
is computed over the entire domain.
13+
14+
Attributes:
15+
field: the species for which the profile is computed.
16+
subdomain: the volume subdomain to compute the profile on. If None, the profile
17+
is computed over the entire domain.
18+
x: the coordinates along which the profile is computed.
19+
data: the computed profile data.
20+
"""
21+
22+
x: np.ndarray
23+
data: list
24+
field: Species
25+
subdomain: VolumeSubdomain | None
26+
_dofs: np.ndarray
27+
_sort_coords: np.ndarray
28+
29+
def __init__(self, field: Species, subdomain: VolumeSubdomain = None):
30+
self.field = field
31+
self.data = []
32+
self.x = None
33+
self.subdomain = subdomain
34+
35+
self._dofs = None
36+
self._sort_coords = None

src/festim/hydrogen_transport_problem.py

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ufl
1313
from dolfinx import fem
1414
from scifem import BlockedNewtonSolver
15+
import numpy as np
1516

1617
import festim.boundary_conditions
1718
import festim.problem
@@ -971,12 +972,25 @@ def post_processing(self):
971972
export.write(t=float(self.t))
972973
if isinstance(export, exports.XDMFExport):
973974
export.write(float(self.t))
975+
if isinstance(export, exports.Profile1DExport):
976+
if export._dofs is None:
977+
index = self.species.index(export.field)
978+
V0, export._dofs = self.u.function_space.sub(index).collapse()
979+
coords = V0.tabulate_dof_coordinates()[:, 0]
980+
export._sort_coords = np.argsort(coords)
981+
x = coords[export._sort_coords]
982+
export.x = x
983+
984+
c = self.u.x.array[export._dofs][export._sort_coords]
985+
986+
export.data.append(c)
974987

975988

976989
class HydrogenTransportProblemDiscontinuous(HydrogenTransportProblem):
977990
interfaces: list[_subdomain.Interface]
978991
surface_to_volume: dict
979992
method_interface: str = "penalty"
993+
subdomain_to_species: dict
980994

981995
def __init__(
982996
self,
@@ -1031,6 +1045,7 @@ def __init__(
10311045
)
10321046
self.interfaces = interfaces or []
10331047
self.surface_to_volume = surface_to_volume or {}
1048+
self.subdomain_to_species = {} # maps subdomain to species defined in it
10341049

10351050
def initialise(self):
10361051
# check that all species have a list of F.VolumeSubdomain as this is
@@ -1162,13 +1177,13 @@ def define_function_spaces(
11621177
Defaults to 1.
11631178
"""
11641179
# get number of species defined in the subdomain
1165-
all_species = [
1180+
self.subdomain_to_species[subdomain] = [
11661181
species for species in self.species if subdomain in species.subdomains
11671182
]
11681183

11691184
# instead of using the set function we use a list to keep the order
11701185
unique_species = []
1171-
for species in all_species:
1186+
for species in self.subdomain_to_species[subdomain]:
11721187
if species not in unique_species:
11731188
unique_species.append(species)
11741189
nb_species = len(unique_species)
@@ -1364,7 +1379,7 @@ def mixed_term(u, v, n):
13641379
h_1 = 2 * cr(res[1])
13651380

13661381
all_mobile_species = [spe for spe in self.species if spe.mobile]
1367-
1382+
# TODO only do this if the species in defined in both domains of the interface?
13681383
for H in all_mobile_species:
13691384
v_b = H.subdomain_to_test_function[subdomain_0](res[0])
13701385
v_t = H.subdomain_to_test_function[subdomain_1](res[1])
@@ -1426,17 +1441,13 @@ def mixed_term(u, v, n):
14261441
interface.id
14271442
) - 0.5 * mixed_term(
14281443
v_b, (u_b / K_b - u_t / K_t), n_0
1429-
) * dInterface(
1430-
interface.id
1431-
)
1444+
) * dInterface(interface.id)
14321445

14331446
F_1 = +0.5 * mixed_term((u_b + u_t), v_t, n_0) * dInterface(
14341447
interface.id
14351448
) - 0.5 * mixed_term(
14361449
v_t, (u_b / K_b - u_t / K_t), n_0
1437-
) * dInterface(
1438-
interface.id
1439-
)
1450+
) * dInterface(interface.id)
14401451
F_0 += (
14411452
2
14421453
* gamma
@@ -1631,6 +1642,25 @@ def post_processing(self):
16311642
if export.filename is not None:
16321643
export.write(t=float(self.t))
16331644

1645+
elif isinstance(export, exports.Profile1DExport):
1646+
assert export.subdomain, (
1647+
"Profile1DExport requires a subdomain to be set"
1648+
)
1649+
u = export.subdomain.u
1650+
if export._dofs is None:
1651+
index = self.subdomain_to_species[export.subdomain].index(
1652+
export.field
1653+
)
1654+
V0, export._dofs = u.function_space.sub(index).collapse()
1655+
coords = V0.tabulate_dof_coordinates()[:, 0]
1656+
export._sort_coords = np.argsort(coords)
1657+
x = coords[export._sort_coords]
1658+
export.x = x
1659+
1660+
c = u.x.array[export._dofs][export._sort_coords]
1661+
1662+
export.data.append(c)
1663+
16341664
def iterate(self):
16351665
"""Iterates the model for a given time step"""
16361666
if self.show_progress_bar:

test/test_profile.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import festim as F
2+
import numpy as np
3+
4+
5+
def test_profile():
6+
my_model = F.HydrogenTransportProblem()
7+
8+
protium = F.Species("H")
9+
deuterium = F.Species("D")
10+
tritium = F.Species("T")
11+
my_model.species = [protium, deuterium, tritium]
12+
13+
my_model.mesh = F.Mesh1D(np.linspace(0, 1, 100))
14+
15+
left_surf = F.SurfaceSubdomain1D(id=1, x=0)
16+
right_surf = F.SurfaceSubdomain1D(id=2, x=1)
17+
18+
# assumes the same diffusivity for all species
19+
material = F.Material(D_0=1, E_D=0)
20+
21+
vol = F.VolumeSubdomain1D(id=1, borders=[0, 1], material=material)
22+
23+
my_model.subdomains = [vol, left_surf, right_surf]
24+
25+
my_model.boundary_conditions = [
26+
# Protium BCs
27+
F.FixedConcentrationBC(left_surf, value=10, species=protium),
28+
F.FixedConcentrationBC(right_surf, value=0, species=protium),
29+
# Deuterium BCs
30+
F.FixedConcentrationBC(left_surf, value=5, species=deuterium),
31+
F.FixedConcentrationBC(right_surf, value=0, species=deuterium),
32+
# Tritium BCs
33+
F.FixedConcentrationBC(left_surf, value=0, species=tritium),
34+
F.FixedConcentrationBC(right_surf, value=2, species=tritium),
35+
]
36+
37+
my_model.temperature = 300
38+
39+
my_model.settings = F.Settings(atol=1e-10, rtol=1e-10, final_time=5)
40+
41+
my_model.settings.stepsize = F.Stepsize(1)
42+
43+
my_model.exports = [
44+
F.Profile1DExport(protium),
45+
F.Profile1DExport(deuterium),
46+
]
47+
48+
my_model.initialise()
49+
my_model.run()
50+
51+
assert my_model.exports[0].x is not None
52+
assert my_model.exports[1].x is not None
53+
assert len(my_model.exports[0].data) > 0
54+
assert len(my_model.exports[1].data) > 0
55+
56+
57+
def test_profile_discontinuous():
58+
my_model = F.HydrogenTransportProblemDiscontinuous()
59+
60+
protium = F.Species("H")
61+
deuterium = F.Species("D")
62+
tritium = F.Species("T")
63+
my_model.species = [protium, deuterium, tritium]
64+
65+
vertices_left = np.linspace(0, 0.5, 50)
66+
vertices_right = np.linspace(0.5, 1, 50)
67+
vertices = np.concatenate((vertices_left, vertices_right))
68+
69+
my_model.mesh = F.Mesh1D(vertices)
70+
71+
left_surf = F.SurfaceSubdomain1D(id=1, x=0)
72+
right_surf = F.SurfaceSubdomain1D(id=2, x=1)
73+
74+
# assumes the same diffusivity for all species
75+
material_left = F.Material(D_0=1, E_D=0, K_S_0=1, E_K_S=0)
76+
material_right = F.Material(D_0=1, E_D=0, K_S_0=2, E_K_S=0)
77+
78+
vol1 = F.VolumeSubdomain1D(id=1, borders=[0, 0.5], material=material_left)
79+
vol2 = F.VolumeSubdomain1D(id=2, borders=[0.5, 1], material=material_right)
80+
81+
my_model.interfaces = [
82+
F.Interface(id=3, subdomains=[vol1, vol2], penalty_term=1000)
83+
]
84+
85+
my_model.subdomains = [vol1, vol2, left_surf, right_surf]
86+
87+
for spe in my_model.species:
88+
spe.subdomains = [vol1, vol2]
89+
90+
my_model.surface_to_volume = {
91+
left_surf: vol1,
92+
right_surf: vol2,
93+
}
94+
95+
my_model.boundary_conditions = [
96+
# Protium BCs
97+
F.FixedConcentrationBC(left_surf, value=10, species=protium),
98+
F.FixedConcentrationBC(right_surf, value=0, species=protium),
99+
# Deuterium BCs
100+
F.FixedConcentrationBC(left_surf, value=5, species=deuterium),
101+
F.FixedConcentrationBC(right_surf, value=0, species=deuterium),
102+
# Tritium BCs
103+
F.FixedConcentrationBC(left_surf, value=0, species=tritium),
104+
F.FixedConcentrationBC(right_surf, value=2, species=tritium),
105+
]
106+
107+
my_model.temperature = 300
108+
109+
my_model.settings = F.Settings(atol=1e-10, rtol=1e-10, final_time=0.5)
110+
111+
my_model.settings.stepsize = F.Stepsize(0.1)
112+
113+
my_model.exports = [
114+
F.Profile1DExport(field=protium, subdomain=vol1),
115+
F.Profile1DExport(field=deuterium, subdomain=vol1),
116+
F.Profile1DExport(field=tritium, subdomain=vol1),
117+
F.Profile1DExport(field=protium, subdomain=vol2),
118+
F.Profile1DExport(field=deuterium, subdomain=vol2),
119+
F.Profile1DExport(field=tritium, subdomain=vol2),
120+
]
121+
122+
my_model.initialise()
123+
my_model.run()
124+
125+
for export in my_model.exports:
126+
assert export.x is not None
127+
assert len(export.data) > 0

0 commit comments

Comments
 (0)