-
Notifications
You must be signed in to change notification settings - Fork 40
Custom derived quantity #1138
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
Custom derived quantity #1138
Changes from all commits
a08bd81
1adf3f2
75c94d7
6fadd59
6396a08
7fb33cd
793655a
a007328
bd2cd38
ec16a28
4cb577d
7f1ca11
15baccf
7852681
6782801
bed9596
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| from collections.abc import Callable | ||
|
|
||
| import ufl | ||
| from dolfinx import fem | ||
| from scifem import assemble_scalar | ||
|
|
||
| from festim.exports.derived_quantity import DerivedQuantity | ||
| from festim.subdomain.surface_subdomain import SurfaceSubdomain | ||
| from festim.subdomain.volume_subdomain import VolumeSubdomain | ||
|
|
||
|
|
||
| class CustomQuantity(DerivedQuantity): | ||
| r""" | ||
| Export CustomQuantity. | ||
|
|
||
| Args: | ||
| expr: function that returns a UFL expression | ||
| subdomain: subdomain on which the quantity is evaluated | ||
| title: title of the exported quantity | ||
| filename: name of the file to which the quantity is exported | ||
|
|
||
| Attributes: | ||
| expr: function that returns a UFL expression | ||
| subdomain: subdomain on which the quantity is evaluated | ||
| title: title of the exported quantity | ||
| filename: name of the file to which the quantity is exported | ||
| t: list of time values | ||
| data: list of values of the quantity | ||
|
|
||
| Usage: | ||
|
|
||
| .. testcode:: | ||
|
|
||
| import numpy as np | ||
| import festim as F | ||
|
|
||
| material = F.Material(D_0=1, E_D=0) | ||
| volume = F.VolumeSubdomain(id=1, material=material) | ||
| surface = F.SurfaceSubdomain(id=1, locator=lambda x: np.isclose(x[1], 1)) | ||
|
|
||
| def total_concentration(**kwargs): | ||
| return kwargs["A"] + kwargs["B"] | ||
|
|
||
| quantity = F.CustomQuantity( | ||
| expr=total_concentration, | ||
| subdomain=volume, | ||
| title="Total quantity", | ||
| ) | ||
|
|
||
| surface_quantity = F.CustomQuantity( | ||
| expr=lambda **kwargs: -kwargs["D_A"] * ufl.dot( | ||
| ufl.grad(kwargs["A"]), kwargs["n"] | ||
| ), | ||
| subdomain=surface, | ||
| title="Surface flux", | ||
| ) | ||
|
|
||
| The callable passed to ``expr`` receives keyword arguments assembled by the | ||
| problem class. Common entries are: | ||
|
|
||
| ``A``, ``B``, ... | ||
| Concentrations of the species present in the problem (here A and B). | ||
| ``n`` | ||
| The facet normal on the selected surface subdomain. | ||
| ``T`` | ||
| The temperature field. | ||
| ``D_A``, ``D_B``, ... | ||
| Species-specific diffusion coefficients. | ||
| ``D`` | ||
| The diffusion coefficient data, either a single field for one species or | ||
| a dictionary keyed by species name when several species are present. | ||
| ``x`` | ||
| The spatial coordinate (x[0], x[1], x[2]). | ||
|
|
||
| For a surface quantity, the returned UFL expression can represent a flux such as | ||
|
|
||
| .. math:: | ||
|
|
||
| q = -D\,\nabla c \cdot n | ||
|
|
||
| and FESTIM will assemble | ||
|
|
||
| .. math:: | ||
|
|
||
| Q = \int_{\Gamma} q\,\mathrm{d}\Gamma | ||
|
|
||
| over the selected surface subdomain. | ||
|
|
||
| The expression returned by ``expr`` is treated as an integrand and assembled over | ||
| the chosen subdomain. | ||
|
|
||
| .. math:: | ||
|
|
||
| Q = \int_{\Omega} q\,\mathrm{d}\Omega | ||
|
|
||
| where ``q`` is the UFL expression returned by ``expr`` and ``\Omega`` is either a | ||
| surface or a volume subdomain. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| expr: Callable, | ||
| subdomain: SurfaceSubdomain | VolumeSubdomain, | ||
| title: str = "Custom Quantity", | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we could make this a required arg instead? happy either way
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally never use titles for derived quantities (even the ones given by default), don't know about the others. That's also because I pretty much never write derived quantities to csv. For users using derived quantities like me, i would find it a bit constraining to have to add a title even if you never use it. The only downside of having this default title is that if a user exports two custom quantities to a csv then they would both be written as "Custom Quantity" in the header. Two solutions:
What do you think?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tbf, you can give different filenames per quantity too, so maybe not an issue, think its an issue you'd run into once and then change. Dynamic naming would be better, but only if this rare scenario happens, and it would add more logic. Let's just leave it and see what issues we run into, if any |
||
| filename: str | None = None, | ||
| ) -> None: | ||
| super().__init__(filename=filename) | ||
| self.expr = expr | ||
| self.subdomain = subdomain | ||
| self._title = title | ||
| self.ufl_expr = None | ||
|
|
||
| @property | ||
| def title(self): | ||
| return self._title | ||
|
RemDelaporteMathurin marked this conversation as resolved.
|
||
|
|
||
| def compute(self, measure: ufl.Measure, entity_maps: dict | None = None): | ||
| """Computes the value of the custom quantity and appends it to the data list. | ||
|
|
||
| Args: | ||
| measure: volume or surface measure of the model | ||
| entity_maps: entity maps relating parent mesh and submesh | ||
| """ | ||
| if self.ufl_expr is None: | ||
| raise ValueError("The UFL expression has not been evaluated yet.") | ||
|
|
||
| form = fem.form( | ||
| self.ufl_expr * measure(self.subdomain.id), entity_maps=entity_maps | ||
| ) | ||
| self.value = assemble_scalar(form) | ||
| self.data.append(self.value) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import csv | ||
| from abc import ABC, abstractmethod | ||
|
|
||
|
|
||
| class DerivedQuantity(ABC): | ||
| """Base class for all derived quantities. | ||
|
|
||
| Attributes: | ||
| filename: name of the file to which the quantity is exported | ||
| t: list of time values | ||
| data: list of values of the quantity | ||
| """ | ||
|
|
||
| filename: str | None | ||
| t: list[float] | ||
| data: list[float] | ||
|
|
||
| def __init__(self, filename: str | None = None) -> None: | ||
| self.filename = filename | ||
| self.t = [] | ||
| self.data = [] | ||
| self._first_time_export = True | ||
|
|
||
| @property | ||
| @abstractmethod | ||
| def title(self): | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def compute(self, *args, **kwargs): | ||
| pass | ||
|
|
||
| @property | ||
| def filename(self): | ||
| return self._filename | ||
|
|
||
| @filename.setter | ||
| def filename(self, value): | ||
| if value is None: | ||
| self._filename = None | ||
| elif not isinstance(value, str): | ||
| raise TypeError("filename must be of type str") | ||
| elif not value.endswith(".csv") and not value.endswith(".txt"): | ||
| raise ValueError("filename must end with .csv or .txt") | ||
| self._filename = value | ||
|
|
||
| def write(self, t): | ||
| """If the filename doesnt exist yet, create it and write the header, then append | ||
| the time and value to the file.""" | ||
|
|
||
| if self.filename is not None: | ||
| if self._first_time_export: | ||
| header = ["t(s)", f"{self.title}"] | ||
| with open(self.filename, mode="w+", newline="") as file: | ||
| writer = csv.writer(file) | ||
| writer.writerow(header) | ||
| self._first_time_export = False | ||
| with open(self.filename, mode="a", newline="") as file: | ||
| writer = csv.writer(file) | ||
| writer.writerow([t, self.value]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These math blocks render on the docs page, but not with the VSC hover/tooltip function. Maybe this is fine? Otherwise, we could just have the equations in Unicode?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have this in other places too. I didn't know they don't render on the VSC (pylance?) tooltip. That can be a separate issue that we fix (or not) globally