Skip to content

Commit 01ab7f9

Browse files
authored
Merge pull request #940 from jhdark/new_value_fenics
New method to handle and convert user inputs to FEniCS objects
2 parents ccf59be + 4e04da8 commit 01ab7f9

11 files changed

+666
-241
lines changed

src/festim/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@
3636
from .exports.vtx import VTXSpeciesExport, VTXTemperatureExport
3737
from .exports.xdmf import XDMFExport
3838
from .heat_transfer_problem import HeatTransferProblem
39-
from .helpers import as_fenics_constant, get_interpolation_points
39+
from .helpers import (
40+
as_fenics_constant,
41+
as_mapped_function,
42+
as_fenics_interp_expr_and_function,
43+
Value,
44+
get_interpolation_points,
45+
)
4046
from .hydrogen_transport_problem import (
4147
HTransportProblemDiscontinuous,
4248
HydrogenTransportProblem,

src/festim/heat_transfer_problem.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,10 @@ def create_source_values_fenics(self):
134134
"""For each source create the value_fenics"""
135135
for source in self.sources:
136136
# create value_fenics for all source objects
137-
source.create_value_fenics(
138-
mesh=self.mesh.mesh,
137+
source.value.convert_input_value(
138+
function_space=self.function_space,
139139
t=self.t,
140+
up_to_ufl_expr=True,
140141
)
141142

142143
def create_flux_values_fenics(self):
@@ -205,7 +206,9 @@ def create_formulation(self):
205206
# add sources
206207
for source in self.sources:
207208
self.formulation -= (
208-
source.value_fenics * self.test_function * self.dx(source.volume.id)
209+
source.value.fenics_object
210+
* self.test_function
211+
* self.dx(source.volume.id)
209212
)
210213

211214
# add fluxes

src/festim/helpers.py

Lines changed: 253 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
from collections.abc import Callable
2+
from typing import Optional
3+
14
import dolfinx
5+
import numpy as np
6+
import ufl
27
from dolfinx import fem
8+
from packaging import version
39

410

511
def as_fenics_constant(
@@ -17,8 +23,8 @@ def as_fenics_constant(
1723
Raises:
1824
TypeError: if the value is not a float, an int or a dolfinx.Constant
1925
"""
20-
if isinstance(value, (float, int)):
21-
return fem.Constant(mesh, dolfinx.default_scalar_type(value))
26+
if isinstance(value, float | int):
27+
return fem.Constant(mesh, dolfinx.default_scalar_type(float(value)))
2228
elif isinstance(value, fem.Constant):
2329
return value
2430
else:
@@ -27,7 +33,251 @@ def as_fenics_constant(
2733
)
2834

2935

30-
from packaging import version
36+
def as_mapped_function(
37+
value: Callable,
38+
function_space: Optional[fem.functionspace] = None,
39+
t: Optional[fem.Constant] = None,
40+
temperature: Optional[fem.Function | fem.Constant | ufl.core.expr.Expr] = None,
41+
) -> ufl.core.expr.Expr:
42+
"""Maps a user given callable function to the mesh, time or temperature within
43+
festim as needed
44+
45+
Args:
46+
value: the callable to convert
47+
function_space: the function space of the domain, optional
48+
t: the time, optional
49+
temperature: the temperature, optional
50+
51+
Returns:
52+
The mapped function
53+
"""
54+
55+
# Extract the input variable names in the callable function `value`
56+
arguments = value.__code__.co_varnames
57+
58+
kwargs = {}
59+
if "t" in arguments:
60+
kwargs["t"] = t
61+
if "x" in arguments:
62+
x = ufl.SpatialCoordinate(function_space.mesh)
63+
kwargs["x"] = x
64+
if "T" in arguments:
65+
kwargs["T"] = temperature
66+
67+
return value(**kwargs)
68+
69+
70+
def as_fenics_interp_expr_and_function(
71+
value: Callable,
72+
function_space: dolfinx.fem.function.FunctionSpace,
73+
t: Optional[fem.Constant] = None,
74+
temperature: Optional[fem.Function | fem.Constant | ufl.core.expr.Expr] = None,
75+
) -> tuple[fem.Expression, fem.Function]:
76+
"""Takes a user given callable function, maps the function to the mesh, time or
77+
temperature within festim as needed. Then creates the fenics interpolation
78+
expression and function objects
79+
80+
Args:
81+
value: the callable to convert
82+
function_space: The function space to interpolate function over
83+
t: the time, optional
84+
temperature: the temperature, optional
85+
86+
Returns:
87+
fenics interpolation expression, fenics function
88+
"""
89+
90+
mapped_function = as_mapped_function(
91+
value=value, function_space=function_space, t=t, temperature=temperature
92+
)
93+
94+
fenics_interpolation_expression = fem.Expression(
95+
mapped_function,
96+
get_interpolation_points(function_space.element),
97+
)
98+
99+
fenics_object = fem.Function(function_space)
100+
fenics_object.interpolate(fenics_interpolation_expression)
101+
102+
return fenics_interpolation_expression, fenics_object
103+
104+
105+
class Value:
106+
"""
107+
A class to handle input values from users and convert them to a relevent fenics
108+
object
109+
110+
Args:
111+
input_value: The value of the user input
112+
113+
Attributes:
114+
input_value : The value of the user input
115+
fenics_interpolation_expression : The expression of the user input that is used
116+
to update the `fenics_object`
117+
fenics_object : The value of the user input in fenics format
118+
explicit_time_dependent : True if the user input value is explicitly time
119+
dependent
120+
temperature_dependent : True if the user input value is temperature dependent
121+
122+
"""
123+
124+
input_value: (
125+
float
126+
| int
127+
| fem.Constant
128+
| np.ndarray
129+
| fem.Expression
130+
| ufl.core.expr.Expr
131+
| fem.Function
132+
)
133+
134+
ufl_expression: ufl.core.expr.Expr
135+
fenics_interpolation_expression: fem.Expression
136+
fenics_object: fem.Function | fem.Constant | ufl.core.expr.Expr
137+
explicit_time_dependent: bool
138+
temperature_dependent: bool
139+
140+
def __init__(self, input_value):
141+
self.input_value = input_value
142+
143+
self.ufl_expression = None
144+
self.fenics_interpolation_expression = None
145+
self.fenics_object = None
146+
147+
def __repr__(self) -> str:
148+
return str(self.input_value)
149+
150+
@property
151+
def input_value(self):
152+
return self._input_value
153+
154+
@input_value.setter
155+
def input_value(self, value):
156+
if value is None:
157+
self._input_value = value
158+
elif isinstance(
159+
value,
160+
float
161+
| int
162+
| fem.Constant
163+
| np.ndarray
164+
| fem.Expression
165+
| ufl.core.expr.Expr
166+
| fem.Function,
167+
):
168+
self._input_value = value
169+
elif callable(value):
170+
self._input_value = value
171+
else:
172+
raise TypeError(
173+
"Value must be a float, int, fem.Constant, np.ndarray, fem.Expression,"
174+
f" ufl.core.expr.Expr, fem.Function, or callable not {value}"
175+
)
176+
177+
@property
178+
def explicit_time_dependent(self) -> bool:
179+
"""Returns true if the value given is time dependent"""
180+
if self.input_value is None:
181+
return False
182+
if isinstance(self.input_value, fem.Constant | ufl.core.expr.Expr):
183+
return False
184+
if callable(self.input_value):
185+
arguments = self.input_value.__code__.co_varnames
186+
return "t" in arguments
187+
else:
188+
return False
189+
190+
@property
191+
def temperature_dependent(self) -> bool:
192+
"""Returns true if the value given is temperature dependent"""
193+
if self.input_value is None:
194+
return False
195+
if isinstance(self.input_value, fem.Constant | ufl.core.expr.Expr):
196+
return False
197+
if callable(self.input_value):
198+
arguments = self.input_value.__code__.co_varnames
199+
return "T" in arguments
200+
else:
201+
return False
202+
203+
def convert_input_value(
204+
self,
205+
function_space: Optional[dolfinx.fem.function.FunctionSpace] = None,
206+
t: Optional[fem.Constant] = None,
207+
temperature: Optional[fem.Function | fem.Constant | ufl.core.expr.Expr] = None,
208+
up_to_ufl_expr: Optional[bool] = False,
209+
):
210+
"""Converts a user given value to a relevent fenics object depending
211+
on the type of the value provided
212+
213+
Args:
214+
function_space: the function space of the fenics object, optional
215+
t: the time, optional
216+
temperature: the temperature, optional
217+
up_to_ufl_expr: if True, the value is only mapped to a function if the input
218+
is callable, not interpolated or converted to a function, optional
219+
"""
220+
if isinstance(
221+
self.input_value, fem.Constant | fem.Function | ufl.core.expr.Expr
222+
):
223+
self.fenics_object = self.input_value
224+
225+
elif isinstance(self.input_value, fem.Expression):
226+
self.fenics_interpolation_expression = self.input_value
227+
228+
elif isinstance(self.input_value, float | int):
229+
self.fenics_object = as_fenics_constant(
230+
value=self.input_value, mesh=function_space.mesh
231+
)
232+
233+
elif callable(self.input_value):
234+
args = self.input_value.__code__.co_varnames
235+
# if only t is an argument, create constant object
236+
if "t" in args and "x" not in args and "T" not in args:
237+
if not isinstance(self.input_value(t=float(t)), float | int):
238+
raise ValueError(
239+
"self.value should return a float or an int, not "
240+
+ f"{type(self.input_value(t=float(t)))} "
241+
)
242+
243+
self.fenics_object = as_fenics_constant(
244+
value=self.input_value(t=float(t)), mesh=function_space.mesh
245+
)
246+
247+
elif up_to_ufl_expr:
248+
self.fenics_object = as_mapped_function(
249+
value=self.input_value,
250+
function_space=function_space,
251+
t=t,
252+
temperature=temperature,
253+
)
254+
255+
else:
256+
self.fenics_interpolation_expression, self.fenics_object = (
257+
as_fenics_interp_expr_and_function(
258+
value=self.input_value,
259+
function_space=function_space,
260+
t=t,
261+
temperature=temperature,
262+
)
263+
)
264+
265+
def update(self, t: float):
266+
"""Updates the value
267+
268+
Args:
269+
t: the time
270+
"""
271+
if callable(self.input_value):
272+
arguments = self.input_value.__code__.co_varnames
273+
274+
if isinstance(self.fenics_object, fem.Constant) and "t" in arguments:
275+
self.fenics_object.value = float(self.input_value(t=t))
276+
277+
elif isinstance(self.fenics_object, fem.Function):
278+
if self.fenics_interpolation_expression is not None:
279+
self.fenics_object.interpolate(self.fenics_interpolation_expression)
280+
31281

32282
# Check the version of dolfinx
33283
dolfinx_version = dolfinx.__version__

0 commit comments

Comments
 (0)