Skip to content
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

Export average surface temperature #949

Open
wants to merge 50 commits into
base: fenicsx
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
67277fd
update rtol to make callable in problem
kaelyndunnell Jan 27, 2025
be656dc
adjust default rtol
kaelyndunnell Jan 27, 2025
4e3874c
adjust default rtol
kaelyndunnell Jan 27, 2025
ffa890d
update atol to make callable in problem.py
kaelyndunnell Feb 10, 2025
9ac79df
fix bug
kaelyndunnell Feb 10, 2025
d3fd8a7
add temp export
kaelyndunnell Feb 22, 2025
eb5cdd4
surface temperature export class
kaelyndunnell Feb 23, 2025
77da114
add surfacetemp to festim attribute
kaelyndunnell Feb 23, 2025
9b20f01
update surfacetemperature class
kaelyndunnell Feb 23, 2025
aa813e5
circular import
kaelyndunnell Feb 23, 2025
b9f700a
fix circular import
kaelyndunnell Feb 23, 2025
d4a5e9f
fix surface temperature export
kaelyndunnell Feb 23, 2025
773b291
add post processing surfacetemp update
kaelyndunnell Feb 23, 2025
b3f4488
update compute method for surface temp
kaelyndunnell Feb 23, 2025
3e70f37
fix double count surface temp
kaelyndunnell Feb 24, 2025
d1b71de
typos
kaelyndunnell Feb 24, 2025
d67fa20
Merge branch 'festim-dev:fenicsx' into fenicsx
kaelyndunnell Feb 24, 2025
6a52a39
add surface temperature export class
kaelyndunnell Feb 24, 2025
4e5e68b
add surface temp to derived quantities test
kaelyndunnell Feb 25, 2025
9a9106d
add better doc, add surface temp test
kaelyndunnell Feb 25, 2025
f03ae7d
add pytest to import
kaelyndunnell Feb 25, 2025
aefcdaa
add my_export quantity
kaelyndunnell Feb 25, 2025
806502a
delete extra line
kaelyndunnell Feb 25, 2025
91f1d5b
fix exported value comparison
kaelyndunnell Feb 25, 2025
1e5e2b0
add temp field value to surface temp
kaelyndunnell Feb 25, 2025
7b0ea6d
add temp field setter
kaelyndunnell Feb 25, 2025
528492c
initialise exports
kaelyndunnell Feb 25, 2025
1971774
remove surface temp from derived quantities
kaelyndunnell Feb 25, 2025
7d687de
change func name, add float and int to temp types
kaelyndunnell Feb 26, 2025
37d59f6
test if test or definition problem
kaelyndunnell Feb 26, 2025
98167a0
fix test
kaelyndunnell Feb 26, 2025
a044ecf
fix test
kaelyndunnell Feb 26, 2025
eb486c3
fix temp field setter
kaelyndunnell Feb 26, 2025
e0b1820
fix setter
kaelyndunnell Feb 26, 2025
9e2937e
simplify setter
kaelyndunnell Feb 26, 2025
38a6ae1
testing export.value
kaelyndunnell Feb 26, 2025
7e8d843
update test
kaelyndunnell Feb 26, 2025
f414bd1
update test
kaelyndunnell Feb 26, 2025
66ad788
uodate test
kaelyndunnell Feb 26, 2025
60eb9e7
update test
kaelyndunnell Feb 26, 2025
c1bac95
update test
kaelyndunnell Feb 26, 2025
03148d1
add test
kaelyndunnell Feb 26, 2025
c955d9f
black formatted
kaelyndunnell Feb 26, 2025
8c2f0bf
add test for title
kaelyndunnell Feb 26, 2025
f6b5ff6
black formatted
kaelyndunnell Feb 26, 2025
339a94a
additional tests
kaelyndunnell Feb 26, 2025
e960b5a
black formatted
kaelyndunnell Feb 26, 2025
97b3b17
add mesh, fix temp_field name
kaelyndunnell Feb 26, 2025
c69e971
fix failing tests
kaelyndunnell Feb 26, 2025
134a8ff
temperature cannot be a string
kaelyndunnell Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/festim/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from .exports.minimum_volume import MinimumVolume
from .exports.surface_flux import SurfaceFlux
from .exports.surface_quantity import SurfaceQuantity
from .exports.surface_temperature import SurfaceTemperature
from .exports.total_surface import TotalSurface
from .exports.total_volume import TotalVolume
from .exports.volume_quantity import VolumeQuantity
Expand Down
2 changes: 2 additions & 0 deletions src/festim/exports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
"AverageSurface",
"AverageVolume",
"TotalVolume",
"SurfaceTemperature"
]

from .average_surface import AverageSurface
from .average_volume import AverageVolume
from .surface_flux import SurfaceFlux
from .surface_quantity import SurfaceQuantity
from .surface_temperature import SurfaceTemperature
from .total_surface import TotalSurface
from .total_volume import TotalVolume
from .volume_quantity import VolumeQuantity
Expand Down
102 changes: 102 additions & 0 deletions src/festim/exports/surface_temperature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import csv
from dolfinx import fem
import ufl
import festim as F

class SurfaceTemperature:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be explicit an name it AverageSurfaceTemperature

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should inherit from SurfaceQuantity

Suggested change
class SurfaceTemperature:
class SurfaceTemperature(F.SurfaceQuantity):

"""Exports the average temperature on a given surface.

Args:
temperature_field (fem.Constant or fem.Function): the temperature field to be computed
surface (int or festim.SurfaceSubdomain): the surface subdomain
filename (str, optional): name of the file to which the average surface temperature is exported

Attributes:
temperature_field (fem.Constant or fem.Function): the temperature field
surface (int or festim.SurfaceSubdomain): the surface subdomain
filename (str): name of the file to which the surface temperature is exported
t (list): list of time values
data (list): list of average temperature values on the surface
"""

def __init__(self, temperature_field, surface, filename: str = None) -> None:
self.temperature_field = temperature_field
self.surface = surface
self.filename = filename

self.t = []
self.data = []
self._first_time_export = True

@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
Comment on lines +31 to +43
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed if inherits from SurfaceQuantity


@property
def surface(self):
return self._surface

@surface.setter
def surface(self, value):
if not isinstance(value, (int, F.SurfaceSubdomain)) or isinstance(value, bool):
raise TypeError("surface should be an int or F.SurfaceSubdomain")
self._surface = value
Comment on lines +45 to +53
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated from SurfaceQuantity. Can be removed when this class inherits from it


@property
def temperature_field(self):
return self._temperature_field

@temperature_field.setter
def temperature_field(self, value):
# check that temperature field is float, int, fem.Constant, fem.Function, or fem.Expression
if not isinstance(value, (fem.Constant, fem.Function, fem.Expression, int, float)):
raise TypeError("field must be of type float, int, fem.Constant, fem.Function, or fem.Expression")

self._temperature_field = value

@property
def title(self):
return f"Temperature surface {self.surface.id}"

def compute(self, ds):
"""Computes the average temperature on the surface.

Args:
ds (ufl.Measure): surface measure of the model
"""
temperature_field = self.temperature_field

surface_integral = fem.assemble_scalar(fem.form(temperature_field * ds(self.surface.id))) # integral over surface

surface_area = fem.assemble_scalar(fem.form(1 * ds(self.surface.id)))

self.value = surface_integral / surface_area # avg temp

self.data.append(self.value)

def write(self, t):
"""Writes the time and temperature value to the file.

Args:
t (float): current time value
"""
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])
Comment on lines +87 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed if inherits from SurfaceQuantity

22 changes: 21 additions & 1 deletion src/festim/hydrogen_transport_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@
a string, find species object in self.species"""

for export in self.exports:

if isinstance(export, exports.SurfaceTemperature):
continue

Check warning on line 374 in src/festim/hydrogen_transport_problem.py

View check run for this annotation

Codecov / codecov/patch

src/festim/hydrogen_transport_problem.py#L374

Added line #L374 was not covered by tests

# if name of species is given then replace with species object
if isinstance(export.field, list):
for idx, field in enumerate(export.field):
Expand Down Expand Up @@ -415,8 +419,11 @@
export.D = D
export.D_expr = D_expr

elif isinstance(export, exports.SurfaceTemperature):
export.temperature_field = self.temperature_fenics

Check warning on line 423 in src/festim/hydrogen_transport_problem.py

View check run for this annotation

Codecov / codecov/patch

src/festim/hydrogen_transport_problem.py#L423

Added line #L423 was not covered by tests

# reset the data and time for SurfaceQuantity and VolumeQuantity
if isinstance(export, (exports.SurfaceQuantity, exports.VolumeQuantity)):
if isinstance(export, (exports.SurfaceQuantity, exports.VolumeQuantity, exports.SurfaceTemperature)):
export.t = []
export.data = []

Expand Down Expand Up @@ -786,6 +793,8 @@
if source.temperature_dependent:
source.update(t=t)

surface_temp_processed = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? the indentation is odd, is this a leftover?


def post_processing(self):
"""Post processes the model"""

Expand Down Expand Up @@ -818,6 +827,17 @@
# if filename given write export data to file
if export.filename is not None:
export.write(t=float(self.t))

elif isinstance(export, exports.SurfaceTemperature):
export.compute(self.ds) # compute surface temp

Check warning on line 832 in src/festim/hydrogen_transport_problem.py

View check run for this annotation

Codecov / codecov/patch

src/festim/hydrogen_transport_problem.py#L832

Added line #L832 was not covered by tests

export.t.append(float(self.t)) # update export time

Check warning on line 834 in src/festim/hydrogen_transport_problem.py

View check run for this annotation

Codecov / codecov/patch

src/festim/hydrogen_transport_problem.py#L834

Added line #L834 was not covered by tests

# if filename given write export data to file
if export.filename is not None:
export.write(t=float(self.t))

Check warning on line 838 in src/festim/hydrogen_transport_problem.py

View check run for this annotation

Codecov / codecov/patch

src/festim/hydrogen_transport_problem.py#L837-L838

Added lines #L837 - L838 were not covered by tests
Comment on lines +831 to +838
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines are not tested. That's because by design, it's impossible to use SurfaceTemperature in combination with HTransportProblem



elif isinstance(export, exports.VolumeQuantity):
if isinstance(export, (exports.TotalVolume, exports.AverageVolume)):
export.compute(self.dx)
Expand Down
200 changes: 200 additions & 0 deletions test/test_surface_temperature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import numpy as np
import ufl
from dolfinx import fem
import pytest
import os

import festim as F


@pytest.mark.parametrize(
"T_function, expected_values",
[
(3, 3),
(lambda t: t, 3.0),
(lambda x, t: 1.0 + x[0] + t, 10.0),
],
)
def test_surface_temperature_compute_1D(T_function, expected_values):
"""Test that the average surface temperature export computes the correct value."""

# BUILD
L = 6.0
my_mesh = F.Mesh1D(np.linspace(0, L, 10000))
dummy_surface = F.SurfaceSubdomain1D(id=1, x=6)
dummy_volume = F.VolumeSubdomain1D(
id=1, borders=[0, L], material=F.Material(D_0=1, E_D=1, name="dummy")
)
facet_meshtags, temp = my_mesh.define_meshtags(
surface_subdomains=[dummy_surface], volume_subdomains=[dummy_volume]
)

ds = ufl.Measure("ds", domain=my_mesh.mesh, subdomain_data=facet_meshtags)

my_model = F.HydrogenTransportProblem(
mesh=my_mesh,
temperature=T_function,
)
my_model.t = fem.Constant(my_model.mesh.mesh, 0.0)
dt = fem.Constant(my_mesh.mesh, 1.0)

my_model.define_temperature()
my_model.initialise_exports()

# RUN
for i in range(3):
my_model.t.value += dt.value
my_model.update_time_dependent_values()

my_export = F.SurfaceTemperature(
temperature_field=my_model.temperature_fenics, surface=dummy_surface
)
my_export.compute(ds)

# TEST
assert np.isclose(my_export.value, expected_values)


def test_title(tmp_path):
surf_1 = F.SurfaceSubdomain(id=1)
results = "test.csv"
temp = 400
surface_temp = F.SurfaceTemperature(
temperature_field=temp, surface=surf_1, filename=results
)

my_model = F.HydrogenTransportProblem(
temperature=temp,
)
surface_temp.filename = os.path.join(tmp_path, "test.csv")
surface_temp.value = 1

assert surface_temp.title == "Temperature surface 1"


@pytest.mark.parametrize("value", ["my_export.csv", "my_export.txt"])
def test_title_generation(tmp_path, value):
"""Test that the title is made to be written to the header in a csv or txt file"""
my_model = F.HydrogenTransportProblem(mesh=F.Mesh1D(np.linspace(0, 6.0, 10000)),temperature=500)
my_model.define_temperature()

my_export = F.SurfaceTemperature(
filename=os.path.join(tmp_path, f"{value}"),
temperature_field=my_model.temperature_fenics,
surface=F.SurfaceSubdomain1D(id=35, x=1),
)
my_export.value = 2.0
my_export.write(0)
title = np.genfromtxt(my_export.filename, delimiter=",", max_rows=1, dtype=str)

expected_title = "Temperature surface 35"

assert title[1] == expected_title


def test_write_overwrite(tmp_path):
"""Test that the write method overwrites the file if it already exists"""
filename = os.path.join(tmp_path, "my_export.csv")
my_model = F.HydrogenTransportProblem(mesh=F.Mesh1D(np.linspace(0, 6.0, 10000)),temperature=500)
my_model.define_temperature()

my_export = F.SurfaceTemperature(
filename=filename,
temperature_field=my_model.temperature_fenics,
surface=F.SurfaceSubdomain1D(id=35, x=1),
)
my_export.value = 2.0
my_export.write(0)
my_export.write(1)

my_export2 = F.SurfaceTemperature(
filename=filename,
temperature_field=my_model.temperature_fenics,
surface=F.SurfaceSubdomain1D(id=1, x=1),
)
my_export2.value = 3.0
my_export2.write(1)
my_export2.write(2)
my_export2.write(3)

data = np.genfromtxt(filename, delimiter=",", names=True)
file_length = data.size
expected_length = 3

assert file_length == expected_length


def test_filename_setter_raises_TypeError():
"""Test that a TypeError is raised when the filename is not a string"""

with pytest.raises(TypeError, match="filename must be of type str"):
my_model = F.HydrogenTransportProblem(mesh=F.Mesh1D(np.linspace(0, 6.0, 10000)),temperature=500)
my_model.define_temperature()

F.SurfaceTemperature(
filename=1,
temperature_field=my_model.temperature_fenics,
surface=F.SurfaceSubdomain1D(id=1, x=1),
)


def test_filename_setter_raises_ValueError(tmp_path):
"""Test that a ValueError is raised when the filename does not end with .csv or .txt"""

with pytest.raises(ValueError):
my_model = F.HydrogenTransportProblem(mesh=F.Mesh1D(np.linspace(0, 6.0, 10000)),temperature=500)
my_model.define_temperature()

F.SurfaceTemperature(
filename=os.path.join(tmp_path, "my_export.xdmf"),
temperature_field=my_model.temperature_fenics,
surface=F.SurfaceSubdomain1D(id=1, x=1),
)


def test_field_setter_raises_TypeError():
"""Test that a TypeError is raised when the field is not an int, float, fem.Constant, fem.Expression, or fem.Function"""

with pytest.raises(TypeError):

F.SurfaceTemperature(
temperature_field="str",
surface=F.SurfaceSubdomain1D(id=1, x=1),
)


@pytest.mark.parametrize("value", ["my_export.csv", "my_export.txt"])
def test_writer(tmp_path, value):
"""Test that the writes values at each timestep to either a csv or txt file"""
my_model = F.HydrogenTransportProblem(mesh=F.Mesh1D(np.linspace(0, 6.0, 10000)),temperature=500)
my_model.define_temperature()

my_export = F.SurfaceTemperature(
filename=os.path.join(tmp_path, f"{value}"),
temperature_field=my_model.temperature_fenics,
surface=F.SurfaceSubdomain1D(id=1, x=0),
)
my_export.value = 2.0

for i in range(10):
my_export.write(i)
file_length = len(np.genfromtxt(my_export.filename, delimiter=","))

expected_length = i + 2

assert file_length == expected_length


def test_surface_setter_raises_TypeError():
"""Test that a TypeError is raised when the surface is not a
F.SurfaceSubdomain"""

with pytest.raises(
TypeError, match="surface should be an int or F.SurfaceSubdomain"
):
my_model = F.HydrogenTransportProblem(mesh=F.Mesh1D(np.linspace(0, 6.0, 10000)),temperature=500)
my_model.define_temperature()
F.SurfaceTemperature(
temperature_field=my_model.temperature_fenics,
surface="1",
)
Loading