from festim import (
MinimumVolume,
MaximumVolume,
MinimumSurface,
MaximumSurface,
DerivedQuantity,
)
import fenics as f
import os
import numpy as np
from typing import Union
[docs]
class DerivedQuantities(list):
"""
A list of festim.DerivedQuantity objects
Args:
filename (str, optional): the filename (must end with .csv).
If None, the data will not be exported. Defaults to None.
nb_iterations_between_compute (int, optional): number of
iterations between each derived quantities computation.
Defaults to 1.
nb_iterations_between_exports (int, optional): number of
iterations between each export. If None, the file will be
exported at the last timestep. Defaults to None.
show_units (bool, optional): will show the units of each
derived quantity in the title in export. Defaults to True.
Attributes:
filename (str): the filename.
nb_iterations_between_compute (int): number of
iterations between each derived quantities computation.
nb_iterations_between_exports (int): number of
iterations between each export. If None, the file will be
exported at the last timestep.
show_units (bool): will show the units of each
derived quantity in the title in export
data (list): the data to be exported
t (list): the time steps
"""
def __init__(
self,
*args,
filename: str = None,
nb_iterations_between_compute: int = 1,
nb_iterations_between_exports: int = None,
show_units=True,
) -> None:
# checks that input is list
if len(args) == 0:
super().__init__()
else:
if not isinstance(*args, list):
raise TypeError("festim.DerivedQuantities must be a list")
super().__init__(self._validate_derived_quantity(item) for item in args[0])
self.filename = filename
self.nb_iterations_between_compute = nb_iterations_between_compute
self.nb_iterations_between_exports = nb_iterations_between_exports
self.show_units = show_units
self.data = []
self.t = []
def __setitem__(self, index, item):
super().__setitem__(index, self._validate_derived_quantity(item))
[docs]
def insert(self, index, item):
super().insert(index, self._validate_derived_quantity(item))
[docs]
def append(self, item):
super().append(self._validate_derived_quantity(item))
[docs]
def extend(self, other):
if isinstance(other, type(self)):
super().extend(other)
else:
super().extend(self._validate_derived_quantity(item) for item in other)
def _validate_derived_quantity(self, value):
if isinstance(value, DerivedQuantity):
return value
raise TypeError(
"festim.DerivedQuantities must be a list of festim.DerivedQuantity"
)
@property
def filename(self):
return self._filename
@filename.setter
def filename(self, value):
if value is not None:
if not isinstance(value, str):
raise TypeError("filename must be a string")
if not value.endswith(".csv"):
raise ValueError("filename must end with .csv")
self._filename = value
def make_header(self):
header = ["t(s)"]
for quantity in self:
quantity.show_units = self.show_units
header.append(quantity.title)
return header
def assign_measures_to_quantities(self, dx, ds):
self.volume_markers = dx.subdomain_data()
self.surface_markers = ds.subdomain_data()
for quantity in self:
quantity.dx = dx
quantity.ds = ds
quantity.n = f.FacetNormal(dx.subdomain_data().mesh())
[docs]
def assign_properties_to_quantities(self, materials):
"""Assign properties attributes to all DerivedQuantity objects
(D, S, thermal_cond and H) based on the properties stored in materials
Args:
materials (festim.Materials): the materials
"""
for quantity in self:
quantity.D = materials.D
quantity.S = materials.S
quantity.thermal_cond = materials.thermal_cond
quantity.Q = materials.Q
def compute(self, t):
row = [t]
for quantity in self:
if isinstance(quantity, (MaximumVolume, MinimumVolume)):
value = quantity.compute(self.volume_markers)
elif isinstance(quantity, (MaximumSurface, MinimumSurface)):
value = quantity.compute(self.surface_markers)
else:
value = quantity.compute()
# check if first time writing data
if len(self.data) == 0:
self.data = [self.make_header()]
quantity.data.append(value)
quantity.t.append(t)
row.append(value)
self.data.append(row)
self.t.append(t)
def write(self):
if self.filename is not None:
# if the directory doesn't exist
# create it
dirname = os.path.dirname(self.filename)
if not os.path.exists(dirname):
os.makedirs(dirname, exist_ok=True)
# save the data to csv
np.savetxt(self.filename, np.array(self.data), fmt="%s", delimiter=",")
return True
[docs]
def is_export(self, t, final_time, nb_iterations):
"""Checks if the derived quantities should be exported or not based on
the current time, the final time of simulation and the current number
of iterations
Args:
t (float): the current time
final_time (float): the final time of the simulation
nb_iterations (int): the current number of time steps
Returns:
bool: True if the derived quantities should be exported, else False
"""
if final_time is not None:
nb_its_between_exports = self.nb_iterations_between_exports
if nb_its_between_exports is None:
# export at the end
return np.isclose(t, final_time, atol=0)
else:
# export every N iterations
return nb_iterations % nb_its_between_exports == 0
else:
# if steady state, export
return True
[docs]
def is_compute(self, nb_iterations):
"""Checks if the derived quantities should be computed or not based on
the current number of iterations
Args:
nb_iterations (int): the current number of time steps
Returns:
bool: True if it's time to compute, else False
"""
return nb_iterations % self.nb_iterations_between_compute == 0
[docs]
def filter(
self,
surfaces: Union[list, int] = None,
volumes: Union[list, int] = None,
fields: Union[list, str] = None,
instances: DerivedQuantity = None,
):
"""Finds DerivedQuantity objects that match surfaces, volumes, and instances.
Args:
surfaces (Union[list, int], optional): the surface ids to match.
Defaults to None.
volumes (Union[list, int], optional): the volume ids to match.
Defaults to None.
fields (Union[list, str], optional): the fields to match.
Defaults to None.
instances (DerivedQuantity, optional): the DerivedQuantity
instances to match. Defaults to None.
Returns:
list, DerivedQuantity: if only one quantity matches returns this
quantity, else returs a list of DerivedQuantity
"""
# ensure arguments are list
if surfaces is not None and not isinstance(surfaces, list):
surfaces = [surfaces]
if volumes is not None and not isinstance(volumes, list):
volumes = [volumes]
if fields is not None and not isinstance(fields, list):
fields = [fields]
if instances is not None and not isinstance(instances, list):
instances = [instances]
quantities = []
# iterate through derived_quantities
for quantity in self:
# initialise flags to False
match_surface, match_volume, match_field, match_instance = (
False,
False,
False,
False,
)
# check if matches surface
if surfaces is not None:
if hasattr(quantity, "surface") and quantity.surface in surfaces:
match_surface = True
else:
match_surface = True
# check if matches volume
if volumes is not None:
if hasattr(quantity, "volume") and quantity.volume in volumes:
match_volume = True
else:
match_volume = True
# check if matches field
if fields is not None:
if quantity.field in fields:
match_field = True
else:
match_field = True
# check if matches instance
if instances is not None:
if isinstance(quantity, tuple(instances)):
match_instance = True
else:
match_instance = True
# if all flags are True, append to the list
if match_surface and match_volume and match_field and match_instance:
quantities.append(quantity)
if len(quantities) == 1:
quantities = quantities[0]
return quantities