Skip to content

Commit

Permalink
Merge pull request #676 from KulaginVladimir/main
Browse files Browse the repository at this point in the history
  • Loading branch information
RemDelaporteMathurin authored Jan 15, 2024
2 parents ed6b56b + 7c6ca17 commit 621769f
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 44 deletions.
70 changes: 49 additions & 21 deletions festim/exports/txt_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,39 @@

class TXTExport(festim.Export):
"""
Args:
field (str): the exported field ("solute", "1", "retention",
"T"...)
label (str): label of the field. Will also be the filename.
folder (str): the export folder
filename (str): the filename (must end with .txt).
times (list, optional): if provided, the field will be
exported at these timesteps. Otherwise exports at all
timesteps. Defaults to None.
header_format (str, optional): the format of column headers.
Defautls to ".2e".
"""

def __init__(self, field, label, folder, times=None) -> None:
def __init__(self, field, filename, times=None, header_format=".2e") -> None:
super().__init__(field=field)
if times:
self.times = sorted(times)
else:
self.times = times
self.label = label
self.folder = folder
self.filename = filename
self.header_format = header_format
self._first_time = True

@property
def filename(self):
return f"{self.folder}/{self.label}.txt"
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(".txt"):
raise ValueError("filename must end with .txt")
self._filename = value

def is_it_time_to_export(self, current_time):
if self.times is None:
Expand All @@ -57,11 +66,6 @@ def write(self, current_time, steady):
solution = f.project(self.function, V_DG1)
solution_column = np.transpose(solution.vector()[:])
if self.is_it_time_to_export(current_time):
if steady:
header = "x,t=steady"
else:
header = "x,t={}s".format(current_time)

# if the directory doesn't exist
# create it
dirname = os.path.dirname(self.filename)
Expand All @@ -72,6 +76,10 @@ def write(self, current_time, steady):
# write data
# else append new column to the existing file
if steady or self._first_time:
if steady:
header = "x,t=steady"
else:
header = f"x,t={format(current_time, self.header_format)}s"
x = f.interpolate(f.Expression("x[0]", degree=1), V_DG1)
x_column = np.transpose([x.vector()[:]])
data = np.column_stack([x_column, solution_column])
Expand All @@ -81,7 +89,7 @@ def write(self, current_time, steady):
old_file = open(self.filename)
old_header = old_file.readline().split("\n")[0]
old_file.close()
header = old_header + ",t={}s".format(current_time)
header = old_header + f",t={format(current_time, self.header_format)}s"
# Append new column
old_columns = np.loadtxt(self.filename, delimiter=",", skiprows=1)
data = np.column_stack([old_columns, solution_column])
Expand All @@ -90,16 +98,36 @@ def write(self, current_time, steady):


class TXTExports:
def __init__(self, fields=[], times=[], labels=[], folder=None) -> None:
"""
Args:
fields (list): list of exported fields ("solute", "1", "retention",
"T"...)
filenames (list): list of the filenames for each field (must end with .txt).
times (list, optional): if provided, fields will be
exported at these timesteps. Otherwise exports at all
timesteps. Defaults to None.
header_format (str, optional): the format of column headers.
Defautls to ".2e".
"""

def __init__(
self, fields=[], filenames=[], times=None, header_format=".2e"
) -> None:
msg = "TXTExports class will be depricated in future versions of FESTIM"
warnings.warn(msg, DeprecationWarning)

self.fields = fields
if len(self.fields) != len(labels):
if len(self.fields) != len(filenames):
raise ValueError(
"Number of fields to be exported "
"doesn't match number of labels in txt exports"
"doesn't match number of filenames in txt exports"
)
self.times = sorted(times)
self.labels = labels
self.folder = folder
if times:
self.times = sorted(times)
else:
self.times = times
self.filenames = filenames
self.header_format = header_format
self.exports = []
for function, label in zip(self.fields, self.labels):
self.exports.append(TXTExport(function, label, folder, times))
for function, filename in zip(self.fields, self.filenames):
self.exports.append(TXTExport(function, filename, times, header_format))
15 changes: 15 additions & 0 deletions festim/generic_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from festim.h_transport_problem import HTransportProblem
from fenics import *
import numpy as np
import warnings


class Simulation:
Expand Down Expand Up @@ -273,6 +274,20 @@ def initialise(self):
self.mesh.dx, self.mesh.ds, self.materials
)

# needed to ensure that data is actually exported at TXTExport.times
# see issue 675
for export in self.exports.exports:
if isinstance(export, festim.TXTExport) and export.times:
if not self.dt.milestones:
self.dt.milestones = []
for time in export.times:
if time not in self.dt.milestones:
msg = "To ensure that TXTExport exports data at the desired times "
msg += "TXTExport.times are added to milestones"
warnings.warn(msg)
self.dt.milestones.append(time)
self.dt.milestones.sort()

def run(self, completion_tone=False):
"""Runs the model.
Expand Down
32 changes: 32 additions & 0 deletions test/simulation/test_initialise.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import festim as F
from pathlib import Path


def test_initialise_changes_nb_of_sources():
Expand Down Expand Up @@ -71,3 +72,34 @@ def test_initialise_initialise_dt():

# test
assert my_model.dt.value(2) == 3


def test_TXTExport_times_added_to_milestones(tmpdir):
"""Creates a Simulation object and checks that, if no dt.milestones
are given and TXTExport.times are given, TXTExport.times are
are added to dt.milestones by .initialise()
"""
# tmpdir
d = tmpdir.mkdir("test_folder")

# build
my_model = F.Simulation()
my_model.mesh = F.MeshFromVertices([1, 2, 3])
my_model.materials = F.Material(id=1, D_0=1, E_D=0, thermal_cond=1)
my_model.T = F.Temperature(100)
my_model.dt = F.Stepsize(initial_value=3)
my_model.settings = F.Settings(
absolute_tolerance=1e-10, relative_tolerance=1e-10, final_time=4
)
txt_export = F.TXTExport(
field="solute",
filename="{}/solute_label.txt".format(str(Path(d))),
times=[1, 2, 3],
)
my_model.exports = [txt_export]

# run
my_model.initialise()

# test
assert my_model.dt.milestones == txt_export.times
2 changes: 1 addition & 1 deletion test/system/test_chemical_potential.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def run(h):
my_exports = festim.Exports(
[
festim.TXTExport(
"solute", times=[100], label="solute", folder=str(Path(d))
"solute", times=[100], filename="{}/solute.txt".format(str(Path(d)))
),
]
)
Expand Down
18 changes: 9 additions & 9 deletions test/system/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,17 @@ def test_txt_export_desired_times(tmp_path):
my_model.dt = F.Stepsize(0.1)

my_export = F.TXTExport(
"solute", label="mobile_conc", times=[0.2, 0.5], folder=tmp_path
"solute", times=[0.2, 0.5], filename="{}/mobile_conc.txt".format(tmp_path)
)
my_model.exports = [my_export]

my_model.initialise()
my_model.run()

assert os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert os.path.exists(my_export.filename)

data = np.genfromtxt(
"{}/{}.txt".format(my_export.folder, my_export.label),
my_export.filename,
skip_header=1,
delimiter=",",
)
Expand All @@ -163,16 +163,16 @@ def test_txt_export_all_times(tmp_path):
my_model.T = F.Temperature(500)
my_model.dt = F.Stepsize(0.1)

my_export = F.TXTExport("solute", label="mobile_conc", folder=tmp_path)
my_export = F.TXTExport("solute", filename="{}/mobile_conc.txt".format(tmp_path))
my_model.exports = [my_export]

my_model.initialise()
my_model.run()

assert os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert os.path.exists(my_export.filename)

data = np.genfromtxt(
"{}/{}.txt".format(my_export.folder, my_export.label),
my_export.filename,
skip_header=1,
delimiter=",",
)
Expand All @@ -190,15 +190,15 @@ def test_txt_export_steady_state(tmp_path):
my_model.settings = F.Settings(1e-10, 1e-10, transient=False)
my_model.T = F.Temperature(500)

my_export = F.TXTExport("solute", label="mobile_conc", folder=tmp_path)
my_export = F.TXTExport("solute", filename="{}/mobile_conc.txt".format(tmp_path))
my_model.exports = [my_export]

my_model.initialise()
my_model.run()

assert os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert os.path.exists(my_export.filename)

txt = open("{}/{}.txt".format(my_export.folder, my_export.label))
txt = open(my_export.filename)
header = txt.readline().rstrip()
txt.close()

Expand Down
2 changes: 1 addition & 1 deletion test/system/test_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def run(h):
my_exports = festim.Exports(
[
festim.TXTExport(
"solute", times=[100], label="solute", folder=str(Path(d))
"solute", times=[100], filename="{}/solute.txt".format(str(Path(d)))
),
]
)
Expand Down
41 changes: 33 additions & 8 deletions test/unit/test_exports/test_txt_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ def function_subspace(self):
@pytest.fixture
def my_export(self, tmpdir):
d = tmpdir.mkdir("test_folder")
my_export = TXTExport("solute", "solute_label", str(Path(d)), times=[1, 2, 3])
my_export = TXTExport(
"solute",
times=[1, 2, 3],
filename="{}/solute_label.txt".format(str(Path(d))),
)

return my_export

Expand All @@ -34,37 +38,54 @@ def test_file_exists(self, my_export, function):
my_export.function = function
my_export.write(current_time=current_time, steady=False)

assert os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert os.path.exists(my_export.filename)

def test_file_doesnt_exist(self, my_export, function):
current_time = 10
my_export.function = function
my_export.write(current_time=current_time, steady=False)

assert not os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert not os.path.exists(my_export.filename)

def test_create_folder(self, my_export, function):
"""Checks that write() creates the folder if it doesn't exist"""
current_time = 1
my_export.function = function
my_export.folder += "/folder2"
slash_indx = my_export.filename.rfind("/")
my_export.filename = (
my_export.filename[:slash_indx]
+ "/folder2"
+ my_export.filename[slash_indx:]
)
my_export.write(current_time=current_time, steady=False)

assert os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert os.path.exists(my_export.filename)

def test_subspace(self, my_export, function_subspace):
current_time = 1
my_export.function = function_subspace
my_export.write(current_time=current_time, steady=False)

assert os.path.exists("{}/{}.txt".format(my_export.folder, my_export.label))
assert os.path.exists(my_export.filename)

def test_error_filename_endswith_txt(self, my_export):
with pytest.raises(ValueError, match="filename must end with .txt"):
my_export.filename = "coucou"

def test_error_filename_not_a_str(self, my_export):
with pytest.raises(TypeError, match="filename must be a string"):
my_export.filename = 2


class TestIsItTimeToExport:
@pytest.fixture
def my_export(self, tmpdir):
d = tmpdir.mkdir("test_folder")
my_export = TXTExport("solute", "solute_label", str(Path(d)), times=[1, 2, 3])
my_export = TXTExport(
"solute",
times=[1, 2, 3],
filename="{}/solute_label.txt".format(str(Path(d))),
)

return my_export

Expand All @@ -84,7 +105,11 @@ class TestWhenIsNextTime:
@pytest.fixture
def my_export(self, tmpdir):
d = tmpdir.mkdir("test_folder")
my_export = TXTExport("solute", "solute_label", str(Path(d)), times=[1, 2, 3])
my_export = TXTExport(
"solute",
times=[1, 2, 3],
filename="{}/solute_label.txt".format(str(Path(d))),
)

return my_export

Expand Down
13 changes: 9 additions & 4 deletions test/unit/test_exports/test_txt_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@


class TestWrite:
@pytest.fixture
def my_export(self, tmpdir):
@pytest.fixture(params=[[1, 2, 3], None])
def my_export(self, tmpdir, request):
d = tmpdir.mkdir("test_folder")
my_export = TXTExports(
["solute", "T"], [1, 2, 3], ["solute_label", "T_label"], str(Path(d))
fields=["solute", "T"],
filenames=[
"{}/solute_label.txt".format(str(Path(d))),
"{}/T_label.txt".format(str(Path(d))),
],
times=request.param,
)

return my_export
Expand All @@ -22,4 +27,4 @@ def test_txt_exports_times(self, my_export):

def test_error_when_fields_and_labels_have_different_lengths():
with pytest.raises(ValueError, match="Number of fields to be exported"):
TXTExports(["solute", "T"], [1], ["solute_label"])
TXTExports(["solute", "T"], ["solute_label.txt"], [1])

0 comments on commit 621769f

Please sign in to comment.