import basix
import ufl
from dolfinx import fem
from dolfinx.io import VTXWriter
from festim import boundary_conditions, exports, helpers, problem
from festim import source as _source
[docs]
class HeatTransferProblem(problem.ProblemBase):
def __init__(
self,
mesh=None,
subdomains=None,
initial_condition=None,
boundary_conditions=None,
sources=None,
exports=None,
settings=None,
) -> None:
super().__init__(
mesh=mesh,
sources=sources,
exports=exports,
subdomains=subdomains,
boundary_conditions=boundary_conditions,
settings=settings,
)
self.initial_condition = initial_condition
@property
def sources(self):
return self._sources
@sources.setter
def sources(self, value):
if not all(isinstance(source, _source.HeatSource) for source in value):
raise TypeError("sources must be a list of festim.HeatSource objects")
self._sources = value
@property
def boundary_conditions(self):
return self._boundary_conditions
@boundary_conditions.setter
def boundary_conditions(self, value):
if not all(
isinstance(
bc,
boundary_conditions.FixedTemperatureBC | boundary_conditions.HeatFluxBC,
)
for bc in value
):
raise TypeError(
"boundary_conditions must be a list of festim.FixedTemperatureBC "
"or festim.HeatFluxBC objects"
)
self._boundary_conditions = value
def initialise(self):
self.define_function_space()
self.define_meshtags_and_measures()
self.t = fem.Constant(self.mesh.mesh, 0.0)
if self.settings.transient:
# TODO should raise error if no stepsize is provided
# TODO Should this be an attribute of festim.Stepsize?
self._dt = helpers.as_fenics_constant(
self.settings.stepsize.initial_value, self.mesh.mesh
)
self.define_boundary_conditions()
self.create_source_values_fenics()
self.create_flux_values_fenics()
self.create_initial_conditions()
self.create_formulation()
self.create_solver()
self.initialise_exports()
[docs]
def define_function_space(self):
"""Creates the function space of the model, creates a mixed element if model is
multispecies.
Creates the main solution and previous solution function u and u_n. Create
global DG function spaces of degree 0 and 1 for the global diffusion coefficient
"""
degree = 1
element_CG = basix.ufl.element(
basix.ElementFamily.P,
self.mesh.mesh.basix_cell(),
degree,
basix.LagrangeVariant.equispaced,
)
self.function_space = fem.functionspace(self.mesh.mesh, element_CG)
self.u = fem.Function(self.function_space)
self.u_n = fem.Function(self.function_space)
self.test_function = ufl.TestFunction(self.function_space)
[docs]
def create_source_values_fenics(self):
"""For each source create the value_fenics."""
for source in self.sources:
# create value_fenics for all source objects
source.value.convert_input_value(
function_space=self.function_space,
t=self.t,
up_to_ufl_expr=True,
)
[docs]
def create_flux_values_fenics(self):
"""For each heat flux create the value_fenics."""
for bc in self.boundary_conditions:
# create value_fenics for all F.HeatFluxBC objects
if isinstance(bc, boundary_conditions.HeatFluxBC):
bc.create_value_fenics(
mesh=self.mesh.mesh,
temperature=self.u,
t=self.t,
)
[docs]
def create_initial_conditions(self):
"""For each initial condition, create the value_fenics and assign it to the
previous solution of the condition's species."""
if not self.initial_condition:
return
if not self.settings.transient:
raise ValueError(
"Initial conditions can only be defined for transient simulations"
)
# create value_fenics for condition
self.initial_condition.create_expr_fenics(
mesh=self.mesh.mesh,
function_space=self.function_space,
)
# assign to previous solution of species
self.u_n.interpolate(self.initial_condition.expr_fenics)
[docs]
def initialise_exports(self):
"""Defines the export writers of the model, if field is given as a string, find
species object in self.species."""
for export in self.exports:
if isinstance(export, exports.XDMFExport):
raise NotImplementedError(
"XDMF export is not implemented yet for heat transfer problems"
)
if isinstance(export, exports.VTXTemperatureExport):
export.writer = VTXWriter(
self.u.function_space.mesh.comm,
export.filename,
[self.u],
engine="BP5",
)
[docs]
def post_processing(self):
"""Post processes the model."""
for export in self.exports:
# skip if it isn't time to export
if hasattr(export, "times"):
if not helpers.is_it_time_to_export(
current_time=float(self.t), times=export.times
):
continue
# TODO if export type derived quantity
if isinstance(export, exports.SurfaceQuantity):
raise NotImplementedError(
"SurfaceQuantity export is not implemented yet "
"for heat transfer problems"
)
export.compute(
self.mesh.n,
self.ds,
)
# update export data
export.t.append(float(self.t))
# if filename given write export data to file
if export.filename is not None:
export.write(t=float(self.t))
if isinstance(export, exports.XDMFExport):
export.write(float(self.t))
if isinstance(export, exports.VTXTemperatureExport):
export.writer.write(float(self.t))
def __del__(self):
for export in self.exports:
if isinstance(export, exports.VTXTemperatureExport):
export.writer.close()