Skip to content

Commit

Permalink
Merge pull request #568 from RemDelaporteMathurin/timestamps
Browse files Browse the repository at this point in the history
Simulation time milestones
  • Loading branch information
RemDelaporteMathurin authored Oct 13, 2023
2 parents 5c18f0f + 9035525 commit 13b0959
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 50 deletions.
6 changes: 3 additions & 3 deletions festim/exports/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
16 changes: 5 additions & 11 deletions festim/exports/txt_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion festim/generic_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,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
Expand Down
2 changes: 1 addition & 1 deletion festim/h_transport_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 60 additions & 17 deletions festim/stepsize.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fenics as f
import numpy as np


class Stepsize:
Expand All @@ -15,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__(
Expand All @@ -29,6 +33,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:
Expand All @@ -40,8 +45,20 @@ def __init__(
}
self.initial_value = initial_value
self.value = None
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"""
Expand All @@ -55,20 +72,46 @@ 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
):
self.value.assign((next_milestone - t))

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:
if current_time < milestone:
return milestone
return None
21 changes: 4 additions & 17 deletions test/unit/test_exports/test_txt_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
50 changes: 50 additions & 0 deletions test/unit/test_stepsize.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import festim
import pytest
import numpy as np


class TestAdapt:
Expand Down Expand Up @@ -36,3 +37,52 @@ 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
)

0 comments on commit 13b0959

Please sign in to comment.