From 76be8d46185166719a58c094f98abf44485ea049 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 04:32:17 -0400 Subject: [PATCH 01/10] added milestones simple --- festim/h_transport_problem.py | 2 +- festim/stepsize.py | 51 ++++++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/festim/h_transport_problem.py b/festim/h_transport_problem.py index 99dcd04e9..5f85f3b9b 100644 --- a/festim/h_transport_problem.py +++ b/festim/h_transport_problem.py @@ -248,7 +248,7 @@ def update(self, t, dt): while converged is False: self.u.assign(u_) nb_it, converged = self.solve_once() - if dt.adaptive_stepsize is not None: + if dt.adaptive_stepsize is not None or dt.milestones is not None: dt.adapt(t, nb_it, converged) # Update previous solutions diff --git a/festim/stepsize.py b/festim/stepsize.py index ad88677d4..bb9c89f28 100644 --- a/festim/stepsize.py +++ b/festim/stepsize.py @@ -1,4 +1,5 @@ import fenics as f +import numpy as np class Stepsize: @@ -29,6 +30,7 @@ def __init__( t_stop=None, stepsize_stop_max=None, dt_min=None, + milestones=None, ) -> None: self.adaptive_stepsize = None if stepsize_change_ratio is not None: @@ -40,6 +42,7 @@ def __init__( } self.initial_value = initial_value self.value = None + self.milestones = milestones self.initialise_value() def initialise_value(self): @@ -55,20 +58,36 @@ def adapt(self, t, nb_it, converged): nb_it (int): number of iterations the solver required to converge. converged (bool): True if the solver converged, else False. """ - change_ratio = self.adaptive_stepsize["stepsize_change_ratio"] - dt_min = self.adaptive_stepsize["dt_min"] - stepsize_stop_max = self.adaptive_stepsize["stepsize_stop_max"] - t_stop = self.adaptive_stepsize["t_stop"] - if not converged: - self.value.assign(float(self.value) / change_ratio) - if float(self.value) < dt_min: - raise ValueError("stepsize reached minimal value") - if nb_it < 5: - self.value.assign(float(self.value) * change_ratio) - else: - self.value.assign(float(self.value) / change_ratio) + if self.adaptive_stepsize: + change_ratio = self.adaptive_stepsize["stepsize_change_ratio"] + dt_min = self.adaptive_stepsize["dt_min"] + stepsize_stop_max = self.adaptive_stepsize["stepsize_stop_max"] + t_stop = self.adaptive_stepsize["t_stop"] + if not converged: + self.value.assign(float(self.value) / change_ratio) + if float(self.value) < dt_min: + raise ValueError("stepsize reached minimal value") + if nb_it < 5: + self.value.assign(float(self.value) * change_ratio) + else: + self.value.assign(float(self.value) / change_ratio) - if t_stop is not None: - if t >= t_stop: - if float(self.value) > stepsize_stop_max: - self.value.assign(stepsize_stop_max) + if t_stop is not None: + if t >= t_stop: + if float(self.value) > stepsize_stop_max: + self.value.assign(stepsize_stop_max) + + # adapt for next milestone + next_milestone = self.next_milestone(t) + if next_milestone is not None: + if t + float(self.value) > next_milestone and not np.isclose(t, next_milestone): + print("changing dt") + self.value.assign((next_milestone - t)) + + def next_milestone(self, current_time): + if self.milestones is None: + return None + for milestone in self.milestones: + if current_time < milestone: + return milestone + return None From ab661f3786666b3e53ea9de59ba56b997a9bc6bf Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 04:43:17 -0400 Subject: [PATCH 02/10] txt_export doesn't change dt --- festim/exports/exports.py | 6 +++--- festim/exports/txt_export.py | 12 +++--------- festim/generic_simulation.py | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/festim/exports/exports.py b/festim/exports/exports.py index 450ba63b4..51b3c8650 100644 --- a/festim/exports/exports.py +++ b/festim/exports/exports.py @@ -10,12 +10,11 @@ def __init__(self, exports=[]) -> None: self.final_time = None self.nb_iterations = 0 - def write(self, label_to_function, dt, dx): + def write(self, label_to_function, dx): """writes to file Args: label_to_function (dict): dictionary of labels mapped to solutions - dt (festim.Stepsize): the model's stepsize dx (fenics.Measure): the measure for dx """ for export in self.exports: @@ -61,7 +60,8 @@ def write(self, label_to_function, dt, dx): label_to_function[export.field], self.V_DG1 ) export.function = label_to_function[export.field] - export.write(self.t, dt) + steady = self.final_time == None + export.write(self.t, steady) self.nb_iterations += 1 def initialise_derived_quantities(self, dx, ds, materials): diff --git a/festim/exports/txt_export.py b/festim/exports/txt_export.py index 52a699955..33c8bfe28 100644 --- a/festim/exports/txt_export.py +++ b/festim/exports/txt_export.py @@ -33,7 +33,7 @@ def is_it_time_to_export(self, current_time): if self.times is None: return True for time in self.times: - if current_time == time: + if np.isclose(time, current_time): return True return False @@ -46,14 +46,14 @@ def when_is_next_time(self, current_time): return time return None - def write(self, current_time, dt): + def write(self, current_time, steady): # create a DG1 functionspace V_DG1 = f.FunctionSpace(self.function.function_space().mesh(), "DG", 1) solution = f.project(self.function, V_DG1) if self.is_it_time_to_export(current_time): filename = "{}/{}_{}s.txt".format(self.folder, self.label, current_time) - if dt is None: + if steady: filename = "{}/{}_steady.txt".format(self.folder, self.label) x = f.interpolate(f.Expression("x[0]", degree=1), V_DG1) # if the directory doesn't exist @@ -63,12 +63,6 @@ def write(self, current_time, dt): os.makedirs(dirname, exist_ok=True) np.savetxt(filename, np.transpose([x.vector()[:], solution.vector()[:]])) - # TODO maybe this should be in another method - next_time = self.when_is_next_time(current_time) - if next_time is not None: - if current_time + float(dt.value) > next_time: - dt.value.assign(next_time - current_time) - class TXTExports: def __init__(self, fields=[], times=[], labels=[], folder=None) -> None: diff --git a/festim/generic_simulation.py b/festim/generic_simulation.py index 2fd5b2fb3..e5cb9436a 100644 --- a/festim/generic_simulation.py +++ b/festim/generic_simulation.py @@ -363,7 +363,7 @@ def run_post_processing(self): self.update_post_processing_solutions() self.exports.t = self.t - self.exports.write(self.label_to_function, self.dt, self.mesh.dx) + self.exports.write(self.label_to_function, self.mesh.dx) def update_post_processing_solutions(self): """Creates the post-processing functions by splitting self.u. Projects From d181a9a838a8378f2f27718de2423c2db5c7db1d Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 13:31:56 -0400 Subject: [PATCH 03/10] sort milestones --- festim/stepsize.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/festim/stepsize.py b/festim/stepsize.py index bb9c89f28..f90f4ad40 100644 --- a/festim/stepsize.py +++ b/festim/stepsize.py @@ -42,7 +42,10 @@ def __init__( } self.initial_value = initial_value self.value = None - self.milestones = milestones + if milestones: + self.milestones = sorted(milestones) + else: + self.milestones = milestones self.initialise_value() def initialise_value(self): From cf992d1efedfab98553ed567b5dc74549966ea97 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 13:34:10 -0400 Subject: [PATCH 04/10] setter for milestones --- festim/stepsize.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/festim/stepsize.py b/festim/stepsize.py index f90f4ad40..d6e5db96e 100644 --- a/festim/stepsize.py +++ b/festim/stepsize.py @@ -42,12 +42,20 @@ def __init__( } self.initial_value = initial_value self.value = None - if milestones: - self.milestones = sorted(milestones) - else: - self.milestones = milestones + self.milestones = milestones self.initialise_value() + @property + def milestones(self): + return self._milestones + + @milestones.setter + def milestones(self, value): + if value: + self._milestones = sorted(value) + else: + self._milestones = value + def initialise_value(self): """Creates a fenics.Constant object initialised with self.initial_value and stores it in self.value""" @@ -83,7 +91,9 @@ def adapt(self, t, nb_it, converged): # adapt for next milestone next_milestone = self.next_milestone(t) if next_milestone is not None: - if t + float(self.value) > next_milestone and not np.isclose(t, next_milestone): + if t + float(self.value) > next_milestone and not np.isclose( + t, next_milestone + ): print("changing dt") self.value.assign((next_milestone - t)) From f44f911e8f50da8dcd11f3628d76590122187179 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 13:39:13 -0400 Subject: [PATCH 05/10] fixed test --- test/unit/test_exports/test_txt_export.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/test/unit/test_exports/test_txt_export.py b/test/unit/test_exports/test_txt_export.py index 7b0638605..679c55b3b 100644 --- a/test/unit/test_exports/test_txt_export.py +++ b/test/unit/test_exports/test_txt_export.py @@ -32,7 +32,7 @@ def my_export(self, tmpdir): def test_file_exists(self, my_export, function): current_time = 1 my_export.function = function - my_export.write(current_time=current_time, dt=Stepsize(initial_value=3)) + my_export.write(current_time=current_time, steady=False) assert os.path.exists( "{}/{}_{}s.txt".format(my_export.folder, my_export.label, current_time) @@ -41,7 +41,7 @@ def test_file_exists(self, my_export, function): def test_file_doesnt_exist(self, my_export, function): current_time = 10 my_export.function = function - my_export.write(current_time=current_time, dt=Stepsize(initial_value=3)) + my_export.write(current_time=current_time, steady=False) assert not os.path.exists( "{}/{}_{}s.txt".format(my_export.folder, my_export.label, current_time) @@ -52,29 +52,16 @@ def test_create_folder(self, my_export, function): current_time = 1 my_export.function = function my_export.folder += "/folder2" - my_export.write(current_time=current_time, dt=Stepsize(initial_value=3)) + my_export.write(current_time=current_time, steady=False) assert os.path.exists( "{}/{}_{}s.txt".format(my_export.folder, my_export.label, current_time) ) - def test_dt_is_changed(self, my_export, function): - current_time = 1 - initial_value = 10 - my_export.function = function - dt = Stepsize(initial_value=initial_value) - my_export.write(current_time=current_time, dt=dt) - - assert ( - float(dt.value) == my_export.when_is_next_time(current_time) - current_time - ) - def test_subspace(self, my_export, function_subspace): current_time = 1 my_export.function = function_subspace - my_export.write( - current_time=current_time, dt=Stepsize(initial_value=current_time) - ) + my_export.write(current_time=current_time, steady=False) assert os.path.exists( "{}/{}_{}s.txt".format(my_export.folder, my_export.label, current_time) From 72e28745125d68da60c107c145a0635d243d6f62 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 15:04:41 -0400 Subject: [PATCH 06/10] added documentation and removed print statement --- festim/stepsize.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/festim/stepsize.py b/festim/stepsize.py index d6e5db96e..a4331856c 100644 --- a/festim/stepsize.py +++ b/festim/stepsize.py @@ -16,11 +16,14 @@ class Stepsize: t_stop. Defaults to None. dt_min (float, optional): Minimum stepsize below which an error is raised. Defaults to None. + milestones (list, optional): list of times by which the simulation must + pass. Defaults to None. Attributes: adaptive_stepsize (dict): contains the parameters for adaptive stepsize value (fenics.Constant): value of dt - + milestones (list): list of times by which the simulation must + pass. """ def __init__( @@ -94,7 +97,6 @@ def adapt(self, t, nb_it, converged): if t + float(self.value) > next_milestone and not np.isclose( t, next_milestone ): - print("changing dt") self.value.assign((next_milestone - t)) def next_milestone(self, current_time): From 57eb930110a2175cd670c056a8b59dec4ab38fd6 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Tue, 22 Aug 2023 15:08:14 -0400 Subject: [PATCH 07/10] updated docstrings of TXTExport --- festim/exports/txt_export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/festim/exports/txt_export.py b/festim/exports/txt_export.py index 33c8bfe28..92300b2e7 100644 --- a/festim/exports/txt_export.py +++ b/festim/exports/txt_export.py @@ -15,8 +15,8 @@ class TXTExport(festim.Export): "T"...) label (str): label of the field. Will also be the filename. folder (str): the export folder - times (list, optional): if provided, the stepsize will be modified to - ensure these timesteps are exported. Otherwise exports at all + times (list, optional): if provided, the field will be + exported at these timesteps. Otherwise exports at all timesteps. Defaults to None. """ From 53a41d0dc11fe0599a47579f3995a21a85cd9b36 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 8 Oct 2023 19:39:36 -0400 Subject: [PATCH 08/10] added tests --- test/unit/test_stepsize.py | 45 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/test/unit/test_stepsize.py b/test/unit/test_stepsize.py index 3466092d9..e107a10f8 100644 --- a/test/unit/test_stepsize.py +++ b/test/unit/test_stepsize.py @@ -1,5 +1,6 @@ import festim import pytest +import numpy as np class TestAdapt: @@ -36,3 +37,47 @@ def test_hit_stepsize_max(self, my_stepsize): my_stepsize.adapt(t=6, converged=True, nb_it=2) new_value = float(my_stepsize.value) assert new_value == my_stepsize.adaptive_stepsize["stepsize_stop_max"] + +def test_milestones_are_hit(): + """Test that the milestones are hit at the correct times + """ + # create a StepSize object + step_size = festim.Stepsize(1.0, stepsize_change_ratio=2, milestones=[1.5, 2.0, 10.3]) + + # set the initial time + t = 0.0 + + # create a list of times + times = [] + final_time = 11.0 + + # loop over the time until the final time is reached + while t < final_time: + # call the adapt method being tested + step_size.adapt(t, nb_it=2, converged=True) + + # update the time and number of iterations + t += float(step_size.value) + + # add the current time to the list of times + times.append(t) + + # check that all the milestones are in the list of times + for milestone in step_size.milestones: + assert any(np.isclose(milestone, times)) + + +def test_next_milestone(): + """Test that the next milestone is correct for a given t value + """ + # Create a StepSize object + step_size = festim.Stepsize(milestones=[10.0, 20.0, 30.0]) + + # Set t values + t_values = [5.0, 10.0, 30.0] + expected_milestones = [10.0, 20.0, None] + + # Check that the next milestone is correct for each t value + for t, expected_milestone in zip(t_values, expected_milestones): + next_milestone = step_size.next_milestone(t) + assert np.isclose(next_milestone, expected_milestone) if expected_milestone is not None else next_milestone is None \ No newline at end of file From f72e22b9541bf963a03455f5811551651f9ec8a3 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 8 Oct 2023 19:41:15 -0400 Subject: [PATCH 09/10] black formatting --- test/unit/test_stepsize.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/unit/test_stepsize.py b/test/unit/test_stepsize.py index e107a10f8..de82eb2d8 100644 --- a/test/unit/test_stepsize.py +++ b/test/unit/test_stepsize.py @@ -38,11 +38,13 @@ def test_hit_stepsize_max(self, my_stepsize): new_value = float(my_stepsize.value) assert new_value == my_stepsize.adaptive_stepsize["stepsize_stop_max"] + def test_milestones_are_hit(): - """Test that the milestones are hit at the correct times - """ + """Test that the milestones are hit at the correct times""" # create a StepSize object - step_size = festim.Stepsize(1.0, stepsize_change_ratio=2, milestones=[1.5, 2.0, 10.3]) + step_size = festim.Stepsize( + 1.0, stepsize_change_ratio=2, milestones=[1.5, 2.0, 10.3] + ) # set the initial time t = 0.0 @@ -68,8 +70,7 @@ def test_milestones_are_hit(): def test_next_milestone(): - """Test that the next milestone is correct for a given t value - """ + """Test that the next milestone is correct for a given t value""" # Create a StepSize object step_size = festim.Stepsize(milestones=[10.0, 20.0, 30.0]) @@ -80,4 +81,8 @@ def test_next_milestone(): # Check that the next milestone is correct for each t value for t, expected_milestone in zip(t_values, expected_milestones): next_milestone = step_size.next_milestone(t) - assert np.isclose(next_milestone, expected_milestone) if expected_milestone is not None else next_milestone is None \ No newline at end of file + assert ( + np.isclose(next_milestone, expected_milestone) + if expected_milestone is not None + else next_milestone is None + ) From 9035525954ed05bb17b77f5f120abc7ef198c03a Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sun, 8 Oct 2023 20:39:59 -0400 Subject: [PATCH 10/10] added docstrings --- festim/stepsize.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/festim/stepsize.py b/festim/stepsize.py index a4331856c..ec0cb341e 100644 --- a/festim/stepsize.py +++ b/festim/stepsize.py @@ -99,7 +99,16 @@ def adapt(self, t, nb_it, converged): ): self.value.assign((next_milestone - t)) - def next_milestone(self, current_time): + def next_milestone(self, current_time: float): + """Returns the next milestone that the simulation must pass. + Returns None if there are no more milestones. + + Args: + current_time (float): current time. + + Returns: + float: next milestone. + """ if self.milestones is None: return None for milestone in self.milestones: